Cách nhanh nhất để loại bỏ tất cả các ký tự không in được khỏi Chuỗi Java


81

Cách nhanh nhất để loại bỏ tất cả các ký tự không in được khỏi a Stringtrong Java là gì?

Cho đến nay tôi đã thử và đo trên Chuỗi 138 byte, 131 ký tự:

  • String's replaceAll()- phương pháp chậm nhất
    • 517009 kết quả / giây
  • Biên dịch trước một Mẫu, sau đó sử dụng Matcher's replaceAll()
    • 637836 kết quả / giây
  • Sử dụng StringBuffer, nhận mã điểm bằng cách sử dụng codepointAt()từng cái một và thêm vào StringBuffer
    • 711946 kết quả / giây
  • Sử dụng StringBuffer, nhận các ký tự bằng cách sử dụng charAt()từng cái một và thêm vào StringBuffer
    • 1052964 kết quả / giây
  • Định vị trước một char[]bộ đệm, nhận các ký tự bằng cách sử dụng charAt()từng cái một và lấp đầy bộ đệm này, sau đó chuyển đổi lại thành Chuỗi
    • 2022653 kết quả / giây
  • Định vị trước 2 char[]bộ đệm - cũ và mới, lấy tất cả các ký tự cho Chuỗi hiện có cùng một lúc bằng cách sử dụng getChars(), lặp lại lần lượt từng bộ đệm cũ và điền vào bộ đệm mới, sau đó chuyển đổi bộ đệm mới thành Chuỗi - phiên bản nhanh nhất của riêng tôi
    • 2502502 kết quả / giây
  • Cùng thứ với 2 bộ đệm - chỉ sử dụng byte[], getBytes()và xác định mã hóa là "utf-8"
    • 857485 kết quả / giây
  • Cùng một nội dung với 2 byte[]bộ đệm, nhưng chỉ định mã hóa làm hằng sốCharset.forName("utf-8")
    • 791076 kết quả / giây
  • Cùng một công cụ với 2 byte[]bộ đệm, nhưng chỉ định mã hóa là mã hóa cục bộ 1 byte (hầu như không phải là một việc lành mạnh để làm)
    • 370164 kết quả / giây

Thử tốt nhất của tôi là như sau:

    char[] oldChars = new char[s.length()];
    s.getChars(0, s.length(), oldChars, 0);
    char[] newChars = new char[s.length()];
    int newLen = 0;
    for (int j = 0; j < s.length(); j++) {
        char ch = oldChars[j];
        if (ch >= ' ') {
            newChars[newLen] = ch;
            newLen++;
        }
    }
    s = new String(newChars, 0, newLen);

Bất kỳ suy nghĩ về cách làm cho nó thậm chí còn nhanh hơn?

Điểm thưởng khi trả lời một câu hỏi rất lạ: tại sao việc sử dụng tên bộ ký tự "utf-8" trực tiếp mang lại hiệu suất tốt hơn so với việc sử dụng const tĩnh được cấp phát trước Charset.forName("utf-8")?

Cập nhật

  • Đề xuất từ ratchet freak mang lại hiệu suất ấn tượng 3105590 kết quả / giây, cải thiện + 24%!
  • Đề xuất từ Ed Staub mang lại một cải tiến khác - 3471017 kết quả / giây, + 12% so với mức tốt nhất trước đó.

Cập nhật 2

Tôi đã cố gắng hết sức để thu thập tất cả các giải pháp được đề xuất và các đột biến chéo của nó và xuất bản nó dưới dạng một khung đo điểm chuẩn nhỏ tại github . Hiện tại nó có 17 thuật toán. Một trong số chúng là "đặc biệt" - thuật toán Voo1 ( do người dùng SO cung cấp Voo ) sử dụng các thủ thuật phản xạ phức tạp để đạt được tốc độ siêu sao, nhưng nó làm rối loạn trạng thái của chuỗi JVM, do đó nó được đánh giá chuẩn riêng.

Bạn có thể kiểm tra và chạy nó để xác định kết quả trên hộp của bạn. Đây là bản tóm tắt kết quả mà tôi có được. Thông số kỹ thuật của nó:

  • Debian sid
  • Linux 2.6.39-2-amd64 (x86_64)
  • Java được cài đặt từ một gói sun-java6-jdk-6.24-1, JVM tự nhận dạng là
    • Môi trường thời gian chạy Java (TM) SE (bản dựng 1.6.0_24-b07)
    • Máy chủ ảo Java HotSpot (TM) 64-Bit (bản dựng 19.1-b02, chế độ hỗn hợp)

Các thuật toán khác nhau cho thấy các kết quả cuối cùng khác nhau với một tập dữ liệu đầu vào khác nhau. Tôi đã chạy một điểm chuẩn ở 3 chế độ:

Cùng một chuỗi đơn

Chế độ này hoạt động trên cùng một chuỗi đơn được cung cấp bởi StringSourcelớp dưới dạng hằng số. Cuộc đối đầu là:

 Ops / s │ Thuật toán
──────────┼────────────────────────────────
6 535 947 │ Voo1
──────────┼────────────────────────────────
5 350 454 │ RatchetFreak2EdStaub1GreyCat1
5 249 343 │ EdStaub1
5 002 501 │ EdStaub1GreyCat1
4 859 086 │ ArrayOfCharFromStringCharAt
Chương 4 295 532 │ RatchetFreak1
4 045 307 │ ArrayOfCharFromArrayOfChar
2 790 178 │ RatchetFreak2EdStaub1GreyCat2
2 583 311 │ RatchetFreak2
1 274 859 │ StringBuilderChar
1 138 174 │ StringBuilderCodePoint
  994 727 │ ArrayOfByteUTF8String
  918 611 │ ArrayOfByteUTF8Const
  756 086 │ MatcherReplace
  598 945 │ StringReplaceAll
  460 045 │ ArrayOfByteWindows1251

Ở dạng biểu đồ: (nguồn: greycat.ru )Biểu đồ chuỗi đơn giống nhau

Nhiều chuỗi, 100% chuỗi chứa các ký tự điều khiển

Nhà cung cấp chuỗi nguồn đã tạo trước rất nhiều chuỗi ngẫu nhiên bằng cách sử dụng bộ ký tự (0..127) - do đó hầu như tất cả các chuỗi đều chứa ít nhất một ký tự điều khiển. Các thuật toán đã nhận các chuỗi từ mảng được tạo trước này theo kiểu vòng lặp.

 Ops / s │ Thuật toán
──────────┼────────────────────────────────
2 123 142 │ Voo1
──────────┼────────────────────────────────
1 782 214 │ EdStaub1
1 776 199 │ EdStaub1GreyCat1
1 694 628 │ ArrayOfCharFromStringCharAt
1 481 481 │ ArrayOfCharFromArrayOfChar
1 460 067 │ RatchetFreak2EdStaub1GreyCat1
1 438 435 │ RatchetFreak2EdStaub1GreyCat2
1 366 494 │ RatchetFreak2
1 349 710 │ RatchetFreak1
  893 176 │ ArrayOfByteUTF8String
  817 127 │ ArrayOfByteUTF8Const
  778 089 │ StringBuilderChar
  734 754 │ StringBuilderCodePoint
  377 829 │ ArrayOfByteWindows1251
  224 140 │ MatcherReplace
  211 104 StringReplaceAll

Ở dạng biểu đồ: (nguồn: greycat.ru )Nhiều chuỗi, tập trung 100%

Nhiều chuỗi, 1% chuỗi chứa ký tự điều khiển

Tương tự như trước, nhưng chỉ 1% chuỗi được tạo bằng ký tự điều khiển - 99% khác được tạo bằng cách sử dụng bộ ký tự [32..127], vì vậy chúng hoàn toàn không thể chứa ký tự điều khiển. Tải tổng hợp này là ứng dụng gần nhất với thế giới thực của thuật toán này tại vị trí của tôi.

 Ops / s │ Thuật toán
──────────┼────────────────────────────────
3 711 952 │ Voo1
──────────┼────────────────────────────────
2 851 440 │ EdStaub1GreyCat1
2 455 796 │ EdStaub1
2 426 007 │ ArrayOfCharFromStringCharAt
Chương 2 347 969 │ RatchetFreak2EdStaub1GreyCat2
2 242 152 │ RatchetFreak1
2 171 553 │ ArrayOfCharFromArrayOfChar
1 922 707 │ RatchetFreak2EdStaub1GreyCat1
857 010 │ RatchetFreak2
1 023 751 │ ArrayOfByteUTF8String
  939 055 │ StringBuilderChar
  907 194 │ ArrayOfByteUTF8Const
  841 963 │ StringBuilderCodePoint
  606 465 │ MatcherReplace
  501 555 │ StringReplaceAll
  381 185 │ ArrayOfByteWindows1251

Ở dạng biểu đồ: (nguồn: greycat.ru )Nhiều chuỗi, nồng độ 1%

Thật khó để tôi quyết định xem ai là người đưa ra câu trả lời tốt nhất, nhưng với giải pháp tốt nhất cho ứng dụng trong thế giới thực được đưa ra / truyền cảm hứng bởi Ed Staub, tôi đoán sẽ rất công bằng nếu đánh dấu câu trả lời của anh ấy. Cảm ơn tất cả những người đã tham gia vào việc này, ý kiến ​​đóng góp của bạn rất hữu ích và vô giá. Hãy thoải mái chạy bộ thử nghiệm trên hộp của bạn và đề xuất các giải pháp tốt hơn nữa (giải pháp JNI đang hoạt động, có ai không?).

Người giới thiệu


21
"Câu hỏi này cho thấy nỗ lực nghiên cứu" - hmm ... yeah, vượt qua. +1
Gustav Barkefors

7
StringBuildersẽ nhẹ nhanh hơn StringBuffervì nó là un-đồng bộ, tôi chỉ đề cập đến điều này bởi vì bạn được gắn thẻ nàymicro-optimization

2
@Jarrod Roberson: ok, vì vậy hãy làm cho tất cả các trường chỉ đọc cuối cùng và giải nén s.length()ra khỏi forvòng lặp :-)
home

3
Một số ký tự bên dưới khoảng trắng có thể in được, ví dụ \t\n. Nhiều ký tự trên 127 không thể in được trong bộ ký tự của bạn.
Peter Lawrey

1
bạn đã init bộ đệm chuỗi với dung lượng là s.length()?
ratchet freak

Câu trả lời:


11

Nếu việc nhúng phương thức này vào một lớp không được chia sẻ trên các luồng là hợp lý, thì bạn có thể sử dụng lại bộ đệm:

char [] oldChars = new char[5];

String stripControlChars(String s)
{
    final int inputLen = s.length();
    if ( oldChars.length < inputLen )
    {
        oldChars = new char[inputLen];
    }
    s.getChars(0, inputLen, oldChars, 0);

Vân vân...

Đây là một chiến thắng lớn - 20% hoặc hơn, theo tôi hiểu trường hợp tốt nhất hiện tại.

Nếu điều này được sử dụng trên các chuỗi có khả năng lớn và "rò rỉ" bộ nhớ là một mối quan tâm, thì có thể sử dụng tham chiếu yếu.


Ý tưởng tuyệt vời! Cho đến nay, nó mang lại số lượng lên đến 3471017 chuỗi mỗi giây - tức là cải thiện + 12% so với phiên bản tốt nhất trước đó.
GreyCat

25

sử dụng 1 mảng char có thể hoạt động tốt hơn một chút

int length = s.length();
char[] oldChars = new char[length];
s.getChars(0, length, oldChars, 0);
int newLen = 0;
for (int j = 0; j < length; j++) {
    char ch = oldChars[j];
    if (ch >= ' ') {
        oldChars[newLen] = ch;
        newLen++;
    }
}
s = new String(oldChars, 0, newLen);

và tôi đã tránh các cuộc gọi lặp lại đến s.length();

một tối ưu hóa vi mô khác có thể hoạt động là

int length = s.length();
char[] oldChars = new char[length+1];
s.getChars(0, length, oldChars, 0);
oldChars[length]='\0';//avoiding explicit bound check in while
int newLen=-1;
while(oldChars[++newLen]>=' ');//find first non-printable,
                       // if there are none it ends on the null char I appended
for (int  j = newLen; j < length; j++) {
    char ch = oldChars[j];
    if (ch >= ' ') {
        oldChars[newLen] = ch;//the while avoids repeated overwriting here when newLen==j
        newLen++;
    }
}
s = new String(oldChars, 0, newLen);

1
Cảm ơn! Phiên bản của bạn mang lại 3105590 chuỗi / giây - một cải tiến lớn!
GreyCat

newLen++;: sử dụng preincrement ++newLen;thì sao? - ( ++jtrong vòng lặp cũng vậy). Hãy xem ở đây: stackoverflow.com/questions/1546981/…
Thomas

Thêm finalvào thuật toán này và sử dụng oldChars[newLen++]( ++newLenlà một lỗi - toàn bộ chuỗi sẽ bị lệch 1!) Không mang lại hiệu suất có thể đo lường được (tức là tôi nhận được chênh lệch ± 2..3%, có thể so sánh với sự khác biệt của các lần chạy khác nhau)
GreyCat

@grey Tôi đã tạo một phiên bản khác với một số tối ưu hóa khác
ratchet freak

2
Hừ! Đó là một ý tưởng tuyệt vời! 99,9% các chuỗi trong môi trường sản xuất của tôi sẽ không thực sự yêu cầu tước - Tôi có thể cải thiện nó hơn nữa để loại bỏ char[]phân bổ ngay cả lần đầu tiên và trả lại Chuỗi như hiện tại, nếu không xảy ra việc tước.
GreyCat

11

Tôi đã đánh bại phương pháp tốt nhất hiện tại (giải pháp của freak với mảng được phân bổ trước) khoảng 30% theo các biện pháp của tôi. Làm sao? Bằng cách bán linh hồn của tôi.

Như tôi chắc rằng tất cả mọi người đã theo dõi cuộc thảo luận cho đến nay đều biết điều này vi phạm khá nhiều nguyên tắc lập trình cơ bản, nhưng ôi thôi. Tuy nhiên, điều sau chỉ hoạt động nếu mảng ký tự đã sử dụng của chuỗi không được chia sẻ giữa các chuỗi khác - nếu có thì bất kỳ ai phải gỡ lỗi điều này sẽ có mọi quyền quyết định giết bạn (không có lệnh gọi đến chuỗi con () và sử dụng điều này trên chuỗi chữ điều này sẽ hoạt động vì tôi không hiểu tại sao JVM sẽ tập các chuỗi duy nhất được đọc từ nguồn bên ngoài). Mặc dù đừng quên đảm bảo rằng mã chuẩn không làm điều đó - điều đó rất có thể xảy ra và rõ ràng sẽ giúp giải pháp phản ánh.

Dù sao thì ở đây chúng tôi cũng đi:

    // Has to be done only once - so cache those! Prohibitively expensive otherwise
    private Field value;
    private Field offset;
    private Field count;
    private Field hash;
    {
        try {
            value = String.class.getDeclaredField("value");
            value.setAccessible(true);
            offset = String.class.getDeclaredField("offset");
            offset.setAccessible(true);
            count = String.class.getDeclaredField("count");
            count.setAccessible(true);
            hash = String.class.getDeclaredField("hash");
            hash.setAccessible(true);               
        }
        catch (NoSuchFieldException e) {
            throw new RuntimeException();
        }

    }

    @Override
    public String strip(final String old) {
        final int length = old.length();
        char[] chars = null;
        int off = 0;
        try {
            chars = (char[]) value.get(old);
            off = offset.getInt(old);
        }
        catch(IllegalArgumentException e) {
            throw new RuntimeException(e);
        }
        catch(IllegalAccessException e) {
            throw new RuntimeException(e);
        }
        int newLen = off;
        for(int j = off; j < off + length; j++) {
            final char ch = chars[j];
            if (ch >= ' ') {
                chars[newLen] = ch;
                newLen++;
            }
        }
        if (newLen - off != length) {
            // We changed the internal state of the string, so at least
            // be friendly enough to correct it.
            try {
                count.setInt(old, newLen - off);
                // Have to recompute hash later on
                hash.setInt(old, 0);
            }
            catch(IllegalArgumentException e) {
                e.printStackTrace();
            }
            catch(IllegalAccessException e) {
                e.printStackTrace();
            }
        }
        // Well we have to return something
        return old;
    }

Đối với chuỗi thử nghiệm của tôi được 3477148.18ops/sso sánh với 2616120.89ops/sbiến thể cũ. Tôi khá chắc rằng cách duy nhất để đánh bại điều đó có thể là viết nó bằng C (có lẽ là không) hoặc một số cách tiếp cận hoàn toàn khác mà chưa ai nghĩ đến cho đến nay. Mặc dù tôi hoàn toàn không chắc liệu thời gian có ổn định trên các nền tảng khác nhau hay không - ít nhất là tạo ra kết quả đáng tin cậy trên hộp của tôi (Java7, Win7 x64).


Cảm ơn giải pháp, vui lòng kiểm tra cập nhật câu hỏi - Tôi đã xuất bản khung thử nghiệm của mình và thêm 3 kết quả chạy thử nghiệm cho 17 thuật toán. Thuật toán của bạn luôn ở trên cùng, nhưng nó thay đổi trạng thái bên trong của Chuỗi Java, do đó phá vỡ hợp đồng "Chuỗi không thay đổi" => sẽ khá khó sử dụng nó trong ứng dụng thế giới thực. Thử nghiệm khôn ngoan, vâng, đó là kết quả tốt nhất, nhưng tôi đoán tôi sẽ công bố nó như một đề cử riêng biệt :)
GreyCat

3
@GreyCat Vâng, nó chắc chắn có một số chuỗi lớn được đính kèm và thành thật mà nói, tôi chỉ viết nó lên vì tôi khá chắc rằng không có cách nào đáng chú ý để cải thiện giải pháp tốt nhất hiện tại của bạn hơn nữa. Có những tình huống mà tôi chắc chắn rằng nó sẽ hoạt động tốt (không có chuỗi con hoặc cuộc gọi thực tập trước khi loại bỏ nó), nhưng đó là do kiến ​​thức về một phiên bản Hotspot hiện tại (tức là nó sẽ không thực tập các chuỗi được đọc từ IO - wouldn ' t đặc biệt hữu ích). Nó có thể hữu ích nếu ai thực sự cần những thêm x%, nhưng nếu không nhiều hơn một cơ sở để xem có bao nhiêu bạn vẫn có thể cải thiện;)
Voo

1
Mặc dù tôi đã cố gắng thử một phiên bản JNI nếu tôi có thời gian - chưa bao giờ sử dụng nó cho đến nay sẽ rất thú vị. Nhưng tôi khá chắc chắn rằng nó sẽ chậm hơn vì chi phí gọi bổ sung (chuỗi quá nhỏ) và thực tế là JIT không nên gặp khó khăn như vậy khi tối ưu hóa các chức năng. Chỉ không sử dụng new String()trong trường hợp chuỗi của bạn không bị thay đổi, nhưng tôi nghĩ bạn đã hiểu được điều đó.
Voo

Tôi đã cố gắng làm chính xác điều tương tự trong C thuần túy - và, tốt, nó không thực sự cho thấy nhiều cải tiến so với phiên bản dựa trên phản xạ của bạn. C phiên bản chạy một cái gì đó như + 5..10% nhanh hơn, chứ không phải thực sự là tuyệt vời - Tôi nghĩ rằng nó sẽ có ít nhất như 1.5x-1,7 lần ...
GreyCat

2

Bạn có thể chia nhiệm vụ thành một số nhiệm vụ phụ song song, tùy thuộc vào số lượng của bộ xử lý.


Vâng, tôi cũng đã nghĩ đến nó, nhưng nó sẽ không mang lại bất kỳ lợi ích nào về hiệu suất trong tình huống của tôi - thuật toán loại bỏ này sẽ được gọi trong hệ thống song song rất lớn.
GreyCat

2
Và, bên cạnh đó, tôi có thể đoán rằng việc loại bỏ một vài luồng để xử lý cho mỗi chuỗi 50-100 byte sẽ là một sự quá mức cần thiết.
GreyCat

Vâng, việc tách các chủ đề cho mỗi chuỗi nhỏ không phải là ý kiến ​​hay. Nhưng bộ cân bằng tải có thể cải thiện hiệu suất. BTW, bạn đã kiểm tra hiệu suất với StringBuilder thay vì StringBuffer có hiệu suất thiếu vì nó được đồng bộ hóa.
umbr

Thiết lập sản xuất của tôi chạy tạo ra một số quy trình riêng biệt và sử dụng càng nhiều CPU và lõi song song càng tốt, vì vậy tôi có thể thoải mái sử dụng StringBuilderở mọi nơi mà không gặp bất kỳ sự cố nào.
GreyCat

2

Tôi rất rảnh và đã viết một điểm chuẩn nhỏ cho các thuật toán khác nhau. Nó không hoàn hảo, nhưng tôi lấy tối thiểu 1000 lần chạy một thuật toán đã cho 10000 lần trên một chuỗi ngẫu nhiên (với khoảng 32/200% không phải là bản in theo mặc định). Điều đó sẽ quan tâm đến những thứ như GC, khởi tạo, v.v. - không có quá nhiều chi phí mà bất kỳ thuật toán nào không nên có ít nhất một lần chạy mà không gặp nhiều trở ngại.

Không được tài liệu đặc biệt tốt, nhưng tốt. Tiếp tục - Tôi đã bao gồm cả hai thuật toán của ratchet freak và phiên bản cơ bản. Tại thời điểm này, tôi khởi tạo ngẫu nhiên một chuỗi dài 200 ký tự với các ký tự được phân phối đồng đều trong phạm vi [0, 200).


1 cho các nỗ lực - nhưng bạn nên đã hỏi tôi - Tôi đã có một bộ điểm chuẩn tương tự - đó là nơi tôi đã được thử nghiệm các thuật toán của tôi;)
GreyCat

@GreyCat Vâng, tôi có thể đã, nhưng chỉ ném lại với nhau mà (trong mã anyways hiện có) có lẽ nhanh hơn;)
Voo

1

IANA là người nghiện hiệu suất java mức thấp, nhưng bạn đã thử giải nén vòng lặp chính của mình chưa? Có vẻ như nó có thể cho phép một số CPU thực hiện kiểm tra song song.

Ngoài ra, điều này có một số ý tưởng thú vị để tối ưu hóa.


Tôi nghi ngờ rằng bất kỳ thao tác giải nén nào có thể được thực hiện ở đây, vì có (a) sự phụ thuộc vào các bước sau của thuật toán ở các bước trước đó, (b) Tôi thậm chí chưa nghe nói ai đó thực hiện thao tác mở vòng lặp thủ công trong Java tạo ra bất kỳ kết quả xuất sắc nào; JIT thường làm rất tốt việc cuộn bất cứ thứ gì nó thấy phù hợp với nhiệm vụ. Cảm ơn đã gợi ý và một liên kết, mặc dù :)
GreyCat

0

tại sao việc sử dụng tên bộ ký tự "utf-8" trực tiếp mang lại hiệu suất tốt hơn so với việc sử dụng const Charset.forName ("utf-8") tĩnh được cấp phát trước?

Nếu bạn có nghĩa là, String#getBytes("utf-8")v.v.: Điều này sẽ không nhanh hơn - ngoại trừ một số bộ nhớ đệm tốt hơn - vì Charset.forName("utf-8")được sử dụng nội bộ, nếu bộ ký tự không được lưu vào bộ nhớ đệm.

Một điều có thể là bạn đang sử dụng các bộ ký tự khác nhau (hoặc có thể một số mã của bạn hoạt động rõ ràng) nhưng bộ ký tự được lưu trong bộ nhớ cache StringCodingkhông thay đổi.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.