Tại sao lớp String của Java không triển khai indexOf () hiệu quả hơn?


9

Thực hiện theo câu hỏi dưới đây về Stack Overflow

/programming/5564610/fast-alernative-for-opesindexofopes-str

Tôi đã tự hỏi tại sao java (ít nhất là 6) không sử dụng một triển khai hiệu quả hơn?

Sau đây là mã:

java.lang.String # indexOf (Chuỗi str)

1762    static int indexOf(char[] source, int sourceOffset, int sourceCount,
1763                       char[] target, int targetOffset, int targetCount,
1764                       int fromIndex) {
1765        if (fromIndex >= sourceCount) {
1766            return (targetCount == 0 ? sourceCount : -1);
1767        }
1768        if (fromIndex < 0) {
1769            fromIndex = 0;
1770        }
1771        if (targetCount == 0) {
1772            return fromIndex;
1773        }
1774
1775        char first  = target[targetOffset];
1776        int max = sourceOffset + (sourceCount - targetCount);
1777
1778        for (int i = sourceOffset + fromIndex; i <= max; i++) {
1779            /* Look for first character. */
1780            if (source[i] != first) {
1781                while (++i <= max && source[i] != first);
1782            }
1783
1784            /* Found first character, now look at the rest of v2 */
1785            if (i <= max) {
1786                int j = i + 1;
1787                int end = j + targetCount - 1;
1788                for (int k = targetOffset + 1; j < end && source[j] ==
1789                         target[k]; j++, k++);
1790
1791                if (j == end) {
1792                    /* Found whole string. */
1793                    return i - sourceOffset;
1794                }
1795            }
1796        }
1797        return -1;
1798    }

3
Lưu ý rằng đây không phải là Java 6 nói chung, mà là mã OpenJDK.
Péter Török

1
@ Péter Török, đủ đúng, nhưng giải nén src.zip của jdk1.6.0_23 và nhìn vào tệp String.java tôi thấy cùng một mã chính xác
Yaneeve

1
@Yaneeve, hmmm, thú vị ... nếu tôi là một luật sư của Oracle, tôi chắc chắn sẽ có một vài suy nghĩ về điều này :-)
Péter Török

2
Quy trình này được tối ưu hóa dưới vỏ bọc (khi có sẵn) thông qua các hướng dẫn SSE4.2 - nếu phần cứng của bạn hỗ trợ nó, chỉ cần kích hoạt hỗ trợ với cờ JVM thích hợp.
Nim

2
@Peter - tại sao? Anh ta đã không sao chép mã Java 6 hoặc vi phạm thỏa thuận bí mật / không tiết lộ thông tin thương mại. Ông chỉ nói rằng hai tập tin giống nhau trong lĩnh vực này.
Stephen C

Câu trả lời:


26

"Hiệu quả" là tất cả về sự đánh đổi và thuật toán "tốt nhất" sẽ phụ thuộc vào nhiều yếu tố. Trong trường hợp indexOf(), một trong những yếu tố đó là kích thước chuỗi dự kiến.

Thuật toán của JDK dựa trên tham chiếu được lập chỉ mục đơn giản vào các mảng ký tự hiện có. Knuth-Morris-Pratt mà bạn tham chiếu cần để tạo một cái mới int[]có cùng kích thước với chuỗi đầu vào. Đối với Boyer-Moore , bạn cần một số bảng bên ngoài, ít nhất một trong số đó là hai chiều (tôi nghĩ rằng tôi chưa bao giờ thực hiện BM).

Vì vậy, câu hỏi trở thành: phân bổ các đối tượng bổ sung và xây dựng bảng tra cứu bù đắp bằng hiệu suất tăng của thuật toán? Hãy nhớ rằng, chúng ta không nói về việc thay đổi từ O (N 2 ) sang O (N), mà chỉ đơn giản là giảm số bước thực hiện cho mỗi N.

Và tôi hy vọng rằng các nhà thiết kế JDK đã nói điều gì đó như "đối với các chuỗi ít hơn các ký tự X, cách tiếp cận đơn giản nhanh hơn, chúng tôi không mong đợi việc sử dụng chuỗi thường xuyên lâu hơn thế và những người sử dụng chuỗi dài hơn sẽ biết cách tối ưu hóa tìm kiếm của họ. "


11

Thuật toán tìm kiếm chuỗi hiệu quả tiêu chuẩn mà mọi người đều biết là Boyer-Moore . Trong số những thứ khác, nó yêu cầu xây dựng một bảng chuyển tiếp có cùng kích thước với bộ ký tự của bạn. Trong trường hợp của ASCII, đó là một mảng với 256 mục, là một chi phí không đổi trả cho các chuỗi dài và không làm chậm các chuỗi nhỏ đủ để mọi người quan tâm. Nhưng Java sử dụng các ký tự 2 byte làm cho bảng đó có kích thước 64K. Trong sử dụng bình thường, chi phí này vượt quá tốc độ dự kiến ​​từ Boyer-Moore, vì vậy Boyer-Moore không đáng giá.

Tất nhiên, hầu hết các bảng đó sẽ có cùng mục, vì vậy bạn có thể nghĩ rằng bạn chỉ có thể lưu trữ ngoại lệ theo cách hiệu quả và sau đó cung cấp mặc định cho bất kỳ điều gì không nằm trong ngoại lệ của bạn. Thật không may, cách làm này đi kèm với tra cứu trên đầu khiến chúng quá đắt để có hiệu quả. (Đối với một vấn đề, hãy nhớ rằng nếu một nhánh bất ngờ gây ra sự cố đường ống và những thứ đó có xu hướng đắt tiền.)

Xin lưu ý rằng với Unicode, vấn đề này phụ thuộc nhiều vào mã hóa của bạn. Khi Java được viết, Unicode phù hợp trong 64 K, do đó Java chỉ sử dụng 2 byte cho mỗi ký tự và độ dài của chuỗi chỉ đơn giản là số byte được chia cho 2. (Mã hóa này được gọi là UCS-2.) nhảy tới bất kỳ ký tự cụ thể nào hoặc trích xuất bất kỳ chuỗi con cụ thể nào và không hiệu quả choindexOf()là một vấn đề không. Thật không may, Unicode đã phát triển, do đó, một ký tự Unicode không phải lúc nào cũng phù hợp với một ký tự Java. Điều này khiến Java gặp vấn đề về kích thước mà họ đang cố tránh. (Mã hóa của chúng bây giờ là UTF-16.) Để tương thích ngược, chúng không thể thay đổi kích thước của một ký tự Java, nhưng bây giờ có một meme rằng các ký tự Unicode và các ký tự Java là giống nhau. Họ không, nhưng rất ít lập trình viên Java biết điều đó và thậm chí ít có khả năng gặp phải nó trong cuộc sống hàng ngày. (Lưu ý rằng Windows và .NET đi theo cùng một đường dẫn, vì những lý do tương tự.)

Trong một số ngôn ngữ và môi trường khác, UTF-8 được sử dụng thay thế. Nó có các đặc tính tốt là ASCII là Unicode hợp lệ và Boyer-Moore là hiệu quả. Sự đánh đổi là việc không chú ý đến các vấn đề byte biến đổi đánh vào bạn rõ ràng hơn nhiều so với UTF-16.


IMO, tuyên bố rằng phân bổ 64K "vượt quá tốc độ dự kiến" không có ý nghĩa gì. Một là kích thước bộ nhớ, các chu kỳ CPU khác. Họ không thể so sánh trực tiếp.
Jerry Coffin

1
@ jerry-coffin: Một so sánh trực tiếp là hợp lý. Phải mất các chu kỳ CPU không đáng kể để phân bổ dữ liệu và khởi tạo cấu trúc dữ liệu 64K.
btilly

1
+1 cho mô tả chuyên sâu về chi phí của Boyer-Moore
kdgregory

Khởi tạo rõ ràng là tuyến tính trên kích thước, nhưng ít nhất trong một trường hợp điển hình, phân bổ là tốc độ không đổi.
Jerry Coffin

1

Nó chủ yếu xuất phát từ điều này: sự cải thiện rõ ràng nhất là từ Boyer-Moore, hoặc một số biến thể của nó. BM và biến thể, tuy nhiên, thực sự muốn một giao diện hoàn toàn khác nhau.

Cụ thể, Boyer-Moore và các công cụ phái sinh thực sự hoạt động theo hai bước: đầu tiên bạn thực hiện khởi tạo. Này xây dựng một bảng dựa hoàn toàn trên chuỗi bạn đang tìm kiếm cho . Điều đó tạo ra một bảng mà sau đó bạn có thể sử dụng để tìm kiếm chuỗi đó bao nhiêu lần bạn muốn.

Bạn chắc chắn có thể phù hợp với giao diện hiện có bằng cách ghi nhớ bảng và sử dụng nó cho các tìm kiếm tiếp theo của cùng một chuỗi mục tiêu. Tôi không nghĩ rằng nó sẽ rất phù hợp với mục đích ban đầu của Sun cho chức năng này: đó là một khối xây dựng cấp thấp sẽ không phụ thuộc vào nhiều thứ khác. Làm cho nó trở thành một hàm cấp cao hơn phụ thuộc vào khá nhiều cơ sở hạ tầng khác có nghĩa là (trong số những thứ khác) mà bạn phải đảm bảo rằng không có cơ sở hạ tầng ghi nhớ nào được sử dụng có thể sử dụng tìm kiếm chuỗi con.

Tôi nghĩ rằng kết quả rất có thể của điều đó sẽ chỉ đơn giản là thực hiện lại một cái gì đó như thế này (nghĩa là một thói quen tìm kiếm độc lập) dưới một tên khác, với một thói quen cấp cao hơn dưới tên hiện có. Tất cả mọi thứ được xem xét, tôi nghĩ có lẽ sẽ có ý nghĩa hơn khi chỉ viết một thói quen cấp cao mới với một tên mới.

Thay thế rõ ràng cho điều đó là sử dụng một số phiên bản ghi nhớ rút gọn, ví dụ (chỉ) lưu trữ một bảng tĩnh và sử dụng lại nó nếu chuỗi mục tiêu giống hệt với chuỗi được sử dụng để tạo bảng . Điều đó chắc chắn là có thể, nhưng sẽ rất thiếu tối ưu cho nhiều trường hợp sử dụng. Làm cho nó an toàn chủ đề cũng sẽ không tầm thường.

Một khả năng khác là phơi bày bản chất hai bước của BM tìm kiếm một cách rõ ràng. Tôi nghi ngờ bất kỳ ai cũng thực sự thích ý tưởng đó - nó mang một chi phí khá cao (vụng về, thiếu quen thuộc) và ít hoặc không có lợi cho nhiều trường hợp sử dụng (hầu hết các nghiên cứu về chủ đề này cho thấy độ dài chuỗi trung bình là như thế 20 ký tự).


1
Ngay cả khi bạn thể hiện bản chất hai bước của BM, tôi nghi ngờ rằng bạn sẽ có hiệu suất tốt vì bảng nhảy 64K không thể phù hợp với bộ đệm CPU cấp 1. Chi phí của việc phải nhấn bộ đệm chậm hơn có khả năng cao hơn thực tế là bạn cần ít thao tác hơn.
btilly

@btilly: Điều đó sẽ tạo ra sự khác biệt lớn nếu bạn thực sự có khả năng sử dụng toàn bộ bảng - nhưng ít nhất trong trường hợp điển hình, ~ 1K của bảng sẽ nằm trong bộ đệm và phần còn lại sẽ chỉ bị chạm trong khi khởi tạo.
Jerry Coffin

@ jerry-coffin: Bạn rõ ràng không quan tâm đến việc có thể xử lý văn bản châu Á.
btilly

1
@btilly: Không phải vậy - không phải là tôi không quan tâm; Tôi biết rằng ít nhất là đối với nhiều người dùng, nó ít phổ biến hơn. Ngay cả khi bạn đang xử lý văn bản châu Á, rất hiếm khi tìm kiếm một chuỗi chứa tiếng Hàn 3 loại ký tự tiếng Nhật khác nhau 2 loại ký tự Trung Quốc khác nhau, v.v. Vâng, bảng chữ cái châu Á lớn hơn tiếng Anh, nhưng không , điển hình chuỗi vẫn không chứa hàng chục ngàn ký tự duy nhất. Ví dụ, một chuỗi gồm 20 ký tự, bạn không bao giờ cần nhiều hơn 20 dòng bộ đệm của bảng.
Jerry Coffin

Trong trường hợp xấu nhất, bạn sử dụng một dòng bộ đệm cho mỗi ký tự duy nhất trong chuỗi tìm kiếm.
Jerry Coffin
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.