Tìm kiếm nhị phân branchless nhanh nhất
(mhdm.dev)sb_lower_boundgiữ nguyên giao diện nhưstd::lower_bound, nhưng cho kết quả nhanh hơn tối đa 2 lần so với tìm kiếm nhị phân thông thường khi nhánh so sánh được biên dịch thành conditional move (cmov)- Kết quả so sánh trong tìm kiếm nhị phân thường gây ra branch misprediction vì không thể biết trước vị trí cần tìm, và trên x86 tùy chọn
clang -mllvm -x86-cmov-converter=falsegiúp giảm vấn đề này - Cách cài đặt này giảm
lengthcòn một nửa ở mỗi vòng lặp và chỉ cập nhậtfirsttheo kết quả so sánh để giảm số lệnh, đồng thời luôn thực hiệnk+1phép so sánh trong khoảng2^k <= n < 2^(k+1) - Trong benchmark
clang -cmov, thời gian chạy trung bình làstd::lower_bound61.30ns,sb_lower_bound33.24ns,bb_lower_bound32.73ns; trung bình hình học cũng chênh lệch lớn với các giá trị lần lượt là 39.17ns, 19.81ns, 21.33ns - Với tìm kiếm chuỗi 8 byte khi hàm so sánh chậm,
std::lower_boundđôi khi nhỉnh hơn một chút; còn với mảng lớn, biến thể có thêm prefetching nhanh hơn trung bình khoảng 2.3 lần so vớistd::lower_bound
Cấu trúc cơ bản của sb_lower_bound
sb_lower_boundlà hàm C++ có cùng dạng vớistd::lower_bound- Đầu vào gồm
first,last,value,comp - Giá trị trả về là iterator tại vị trí đầu tiên mà phép so sánh thất bại; nếu mọi phần tử đều thỏa điều kiện thì trả về
last
- Đầu vào gồm
- Vòng lặp cốt lõi giảm
lengthđi một nửa và chỉ di chuyểnfirstvề phía trước khicomp(first[length], value)là đúng - Ở đây “branchless” không có nghĩa là
ifbiến mất, mà làifđó được biên dịch thành lệnh di chuyển có điều kiện nhưcmovthay vì conditional jump - Trong
clang, có thể dùng tùy chọn-mllvm -x86-cmov-converter=falseđể biên dịch mẫu này thành conditional move
Điểm khiến std::lower_bound chậm đi
- Tìm kiếm nhị phân thông thường so sánh phần tử giữa với
value, rồi chọn nửa trái hoặc nửa phải - Khi không biết trước vị trí của giá trị cần tìm,
if (comp(first[half], value))rất dễ trở thành một nhánh khó dự đoán - CPU dùng branch prediction để thực thi trước lệnh tiếp theo, nhưng nếu dự đoán sai thì phải bỏ toàn bộ phần việc đã làm
- Dùng conditional move cho phép chọn giá trị theo kết quả so sánh trong khi giảm số lần dùng conditional jump
clang -cmovcũng có thể biến một phầnif/elsetrongstd::lower_boundthành conditional move, giúp nhanh hơn khoảng 25%gcckhông có tùy chọn tốt để ép tạo conditional move trong cùng tình huống, vàsb_lower_boundhiện cũng không được xuất thành mã branchless bất kể mức tối ưu hóa
Tìm kiếm “tối ưu” xét theo số lần so sánh
- “Tối ưu” ở đây có nghĩa là tìm kiếm nhị phân có số lần so sánh nhỏ nhất
- Với danh sách kích thước
n,std::lower_boundcón+1kết quả khả dĩ:nvị trí phần tử cộng thêm 1 vị trí cuối - Nếu kích thước danh sách là
2^k - 1thì số kết quả khả dĩ là2^k, và vì mỗi phép so sánh cho 1 bit thông tin đúng/sai nên số lần so sánh tối ưu làk - Trong trường hợp “đẹp” với độ dài
2^k - 1, có thể thực hiện tìm kiếm tối ưu bằng một vòng lặp rất ngắn - Nếu độ dài không khớp thì có thể xảy ra truy cập ngoài phạm vi, chẳng hạn khi
valuelà 4 trong[0, 1, 2, 3, 4, 5]
Đặc tính hiệu năng và ràng buộc của sb_lower_bound
- Khi chia đoạn có độ dài chẵn,
sb_lower_boundkhông phải lúc nào cũng bỏ qua đủ nhiều phần tử ngay cả khi kết quả so sánh là đúng - Trong khoảng
2^k <= n < 2^(k+1), nó luôn thực hiệnk+1phép so sánh - Trong cùng khoảng đó,
std::lower_boundthực hiệnkhoặck+1phép so sánh, trung bình khoảnglog2(n+1)lần - Số lần so sánh có thể nhiều hơn, nhưng số lệnh trong vòng lặp ít hơn đáng kể nên tổng thời gian chạy vẫn nhanh hơn
- Nếu hàm so sánh rất chậm thì khác biệt giữa
k+1vàlog2(n+1)phép so sánh có thể ảnh hưởng đến hiệu năng - Để ép conditional move trong
gcc, có thể dùng inline assembly chỉ dành cho x86 vớicmov, nhưng cách đơn giản sẽ tăng số lệnh còn cách khác thì phải viết assembly riêng theo từng kiểu dữ liệu
Biến thể nhanh hơn bb_lower_bound
bb_lower_boundchia đoạn theo cách khác cho đến khi độ dài đạt dạng2^k - 1, rồi tìm kiếm bằng vòng lặp thứ hai nhanh hơnlength & (length + 1)được dùng để kiểm tra độ dài có ở dạng11..1, tức2^k - 1, hay không- Với độ dài không chuẩn, nó dùng giá trị MAGIC
auto step = length / 8 * 6 + 1để nhanh chóng tiến gần tới một đoạn “đẹp” stepnhìn chung nên lớn hơn hoặc bằnglength / 2để thường xuyên chuyển sang vòng lặp nhanh, nhưng nếu quá gầnlengththì sẽ mất lợi thế của tìm kiếm nhị phân- Vì có
break,bb_lower_boundtrở thành dạng có nhánh - Cách dùng bảng tra cứu tính sẵn
stepnhanh nhất cho mọi độ dài vẫn là một hướng chưa được khám phá
Bản cài đặt branchless hoàn toàn không nhanh hơn
- Trên máy 64 bit, vòng lặp của
sb_lower_boundlặp tối đa 64 lần, nên có thể tạo phiên bản “branchless hoàn toàn” bỏ luôn cả kiểm tralengthbằngswitchvà fall-through có chủ đích - Cách này nhảy tới vị trí mã tương ứng với số lần so sánh cần thiết dựa trên
std::bit_width(length) - Hiệu năng thực tế không nhanh hơn
- CPU x86 hiện đại xử lý tốt các nhánh dễ đoán như điều kiện vòng lặp, nên việc bỏ kiểm tra
lengthkhông mang lại lợi ích - Xét cả việc tránh dùng template, macro và tránh sao chép-chỉnh sửa 64 case, tác giả đánh giá vòng lặp thông thường vẫn tốt hơn
Kết quả benchmark
- Thời gian chạy trung bình (ns) với
clang -cmovnhư saustd::lower_: 61.30branchless_lower_: 43.43asm_lower_: 54.32sb_lower_: 33.24sbm_lower_: 35.54bb_lower_: 32.73
- Trung bình hình học (ns) cũng cho thấy
sb_lower_thấp nhấtstd::lower_: 39.17branchless_lower_: 25.14asm_lower_: 31.21sb_lower_: 19.81sbm_lower_: 20.91bb_lower_: 21.33
sbm_lower_boundlà biến thể dùngfirst += comp(first[length], value) * (length + rem)thay choifđể khuyến khíchgccsinh ra conditional move- Tối ưu hóa này có thể biến mất trong các phiên bản
gccsau, nên cần chú thích và lưu ý - Lệnh benchmark sử dụng
g++-10,clang++-10,clang++-10 -mllvm -x86-cmov-converter=falsevà thêm-march=haswell -march=nativehoặc không chỉ định-marchkhông ảnh hưởng lớn đến thứ hạng; bài test được chạy trên Intel i7 Kaby Lake
Đo branch misprediction
- Khi đo bằng
perf, bản chạyclangthông thường ghi nhận khoảng 6.94 tỷ branches và khoảng 1.20 tỷ branch-misses, tương đương tỷ lệ branch-misses 17.34% - Bản chạy
clang -cmovghi nhận khoảng 4.07 tỷ branches và khoảng 35.95 triệu branch-misses, giảm tỷ lệ branch-misses xuống 0.88% -cmovđã loại bỏ khoảng 2.9 tỷ nhánh và khoảng 1.2 tỷ lần dự đoán nhánh sai- Các nhánh bị loại bỏ vốn có xác suất dự đoán sai khoảng 41%
- Đây là giá trị gần với mức 50% có thể kỳ vọng từ những nhánh hoàn toàn không thể dự đoán
Với hàm so sánh chậm thì kết quả khác đi
- Để xem tình huống hàm so sánh chậm hơn, tác giả thử tìm kiếm chuỗi 8 byte
- Ở thời gian chạy trung bình (ns),
std::lower_boundhơi nhanh hơn hoặc tương đươngsb_lower_boundgcc:std::lower_160.01,sb_lower_165.66clang:std::lower_157.71,sb_lower_162.68,bb_lower_157.22clang -cmov:std::lower_156.06,sb_lower_164.71,bb_lower_157.48
- Trong trường hợp này,
std::lower_boundnhanh hơnsb_lower_boundmột chút nhưng khá nhất quán - Để đạt hiệu năng tối ưu, thư viện có thể dùng
sb_lower_boundcho kiểu nguyên thủy và dùngstd::lower_boundcho các trường hợp còn lại
Khác biệt thể hiện trong assembly
- Hot loop của
std::lower_bounddướiclang -cmovcócmova,cmovbevà các conditional move khác, nhưng dùng nhiều lệnh để cập nhật độ dài và vị trí - Hot loop của
sb_lower_boundtính nửa độ dài, phần dư và con trỏ cần di chuyển, rồi cập nhậtfirstbằngcmova - Assembly của
branchless_lower_boundrất ngắn và gọn, nhưng trong thử nghiệm hiệu năng thìsb_lower_boundcho kết quả tốt hơn nhờ overhead thấp hơn
Cập nhật: sb_lower_bound ngắn hơn nữa
- Sau bình luận từ tác giả orlp.net,
sb_lower_boundcó thể được refactor để giảm số lệnh assembly trong hot loop từ 9 xuống 8 - Ý chính là
length - halfbằng vớihalf + length % 2 - Dạng refactor tính
half = length / 2, nếu so sánh đúng thì thực hiệnfirst += length - half, sau đó cập nhậtlength = half - Với
clang -cmov, thời gian chạy trung bình được cải thiện nhẹ từ khoảng 33ns xuống khoảng 32ns
Với mảng lớn, prefetching phát huy hiệu quả
- Prefetching được đề xuất trong phần bình luận là cách đưa trước dữ liệu cần thiết vào cache L1/L2 để giảm độ trễ khi truy cập thật sự
- Ví dụ độ trễ là khoảng 4 chu kỳ cho L1, 12 chu kỳ cho L2, 40 chu kỳ cho L3 và khoảng 200 chu kỳ cho bộ nhớ chính
- Cả
gcclẫnclangđều hỗ trợ__builtin_prefetch() - Nếu prefetch tại vị trí
length / 4thì sẽ lãng phí 1 trong 2 lần; nếu thêm cảlength / 8thì sẽ lãng phí 5 trong 6 lần - Bản thân việc tính vị trí prefetch và gọi nó cũng tạo ra overhead, và trong hot loop đã được rút ngắn thì chi phí này rất quan trọng
- Nhiều chiến lược prefetch khác nhau không giúp ích cho mảng nhỏ hơn 256KB
- Từ 256KB trở lên,
sbp_lower_boundcó thêm prefetching cải thiện thời gian chạy trung bình từ khoảng 32ns xuống khoảng 26ns trong các thử nghiệm tới khoảng 4 triệu phần tử, tức 16MB - Ở bài test mở rộng sau đó lên khoảng 128 triệu phần tử, tức 512MB, phiên bản có prefetch nhanh hơn
std::lower_boundkhoảng 2.3 lần theo thời gian trung bình- Mốc so sánh là
std::lower_boundkhoảng 161ns và bản prefetch khoảng 71ns
- Mốc so sánh là
Quan sát trên tập dữ liệu lớn và các lựa chọn thay thế
- Với kích thước rất lớn,
std::lower_boundbranchless doclang -cmovsinh ra lại chậm hơn bản có nhánh - CPU hiện đại có thể đi theo nhánh đã dự đoán và tiến hành load bộ nhớ cùng speculative execution, điều này trên thực tế có thể hoạt động giống prefetch
sbpm_lower_boundlà phiên bản thêm prefetch vàosbm_lower_bound, đồng thời dùng phép nhân boolean để khiếngccsinh mã branchless- Có hiện tượng nhảy vọt trên đồ thị hiệu năng trong khoảng 1 triệu đến 10 triệu phần tử, cho thấy về mặt lý thuyết vẫn còn chỗ cho một bản cài đặt nhanh hơn
- Tuy vậy, mã prefetch ngày càng phức tạp và có thêm nhiều hằng số magic, nên tác giả cho rằng khả năng đóng góp vào
gcc/libstdc++hayllvm/libc++càng thấp khi độ phức tạp tăng - Một lựa chọn thay thế phá vỡ ràng buộc của
std::lower_boundlà Eytzinger Binary Search, sắp xếp lại mảng đầu vào theo dạng heap trung vị nhị phân để truy vấn thân thiện với cache hơn - Trong bài test int 16-ary tree của Sergey Slotin tại CppCon 2022, cách này nhanh hơn
std::lower_boundtừ 7 đến 15 lần
Mã nguồn và điều kiện sử dụng
- Nếu phần tìm kiếm hoặc so sánh là nút thắt chậm nhất của chương trình, và bộ xử lý khó dự đoán kết quả so sánh, bạn có thể thử tùy chọn
-mllvm -x86-cmov-converter=falsecủaclangtrên x86 - Nếu cần một tìm kiếm nhị phân nhanh hơn, có thể thử
sb_lower_bound; trêngcc,sbm_lower_boundcũng là một lựa chọn - Mã được phát hành theo giấy phép MIT
- Có thể xem mã và benchmark tại github.com/mh-dm/sb_lower_bound/
1 bình luận
Ý kiến trên Hacker News
Mỗi khi thấy mọi người cố gắng loại bỏ nhánh, tôi lại tự hỏi liệu họ có biết rằng việc dự đoán nhánh sai khiến pipeline dài bị khựng lại không phải là yếu tố bắt buộc của kiến trúc CPU hay không
Lý do pipeline dài là vì có rất nhiều phân tích và biến đổi được thực hiện ngay trước khi thực thi, nhưng phần lớn có thể được làm trước vì đây cũng không phải là thuật toán phụ thuộc nhiều vào trạng thái
CPU Transmeta Crusoe từng hoạt động theo kiểu này, và ta có thể tưởng tượng một thế giới nơi không cần bận tâm đến các nhánh
Nhìn sâu hơn thì mọi phép toán đều là một nhánh nhìn vào trạng thái bit rồi thay đổi kết quả, nhưng các nhánh cục bộ như vậy bên trong ALU không phải là nhánh trên pipeline chính, nên không gây hại lớn cho hiệu năng
Tôi nhớ hồi đó cũng đã nói với srk rằng việc chọn chỉ số IPC hay thông lượng sẽ ảnh hưởng đến cách nhìn nhận cái gì là tốt hay xấu
Phe IPC cho rằng nếu tạo ra IPC cao hơn thì phía công nghệ chế tạo sẽ nâng xung nhịp lên và tất cả cùng thắng; còn phe thông lượng thì có cách tiếp cận thực tế hơn, cho rằng định luật Moore đã chết, chạy silicon nhanh hơn sẽ làm nó nóng chảy, nên bên thắng sẽ là bên thiết kế ISA một cách thông minh
Trong 20 năm qua cả hai phía đều có thành công và thất bại, và thật thú vị khi gần đây RISC-V đang quay lại với những câu hỏi kiểu này trong kiến trúc CPU
Đây cũng là nơi tốt để theo dõi cách các ý tưởng superscalar hiện đại được bổ sung dựa trên tính linh hoạt của tập lệnh, và về dài hạn tôi nghĩ hướng này sẽ thắng
Việc chuyển dịch của Transmeta không hề loại bỏ chi phí của nhánh
Tôi nhớ Linus, khi còn làm ở Transmeta, từng nói trong một thread trên comp.arch đại ý rằng “việc của CPU là tạo ra cache miss nhanh nhất có thể”
Cache miss bắt buộc vẫn tồn tại, và không JIT nào có thể loại bỏ chúng
Trong thế giới thực, dù có những cache khổng lồ như hiện nay, cũng không thể tránh được capacity miss
Itanium cũng từng cho rằng phân tích tĩnh có thể loại bỏ chi phí nhánh, và chỉ cần nhớ kết quả của nó ra sao là đủ
Tôi mong các lập trình viên đọc thêm vài cuốn sách về kiến trúc máy tính trước khi tự tin kết luận rằng họ có thể dễ dàng tạo ra thứ tốt hơn các bộ xử lý hiện đại
Tôi cho rằng họ đang đánh giá thấp quy mô nỗ lực trí tuệ đã được đưa vào các bộ xử lý hiện nay ít nhất khoảng 7 chữ số
Một trong số đó là dữ liệu đầu vào được xử lý
Tìm kiếm nhị phân chính là trường hợp như vậy: compiler không biết kết quả sẽ được tìm thấy ở vị trí nào
Một yếu tố khác là vi kiến trúc, đặc biệt là hệ phân cấp cache và cấu hình các đơn vị thực thi
Nếu chuyển sang một ISA có các lệnh tương tự micro-operation của CPU hiện tại, mọi vi kiến trúc đều sẽ phải biên dịch lại
Tuy vậy, về mặt kỹ thuật điều này có thể được giải quyết bằng OS JIT, theo kiểu các chương trình được phân phối dưới dạng bytecode (DXBC, SPIR-V, NVPTX) như GPU hiện nay, rồi driver GPU ở user mode biên dịch lại thành lệnh phần cứng thực tế
Biến số lớn hơn là các luồng CPU khác đang chạy mã mà ta không biết
Ngay cả khi loại bỏ hyperthreading để các lõi độc lập với nhau, vẫn còn các tài nguyên được chia sẻ trên toàn chip như cache L3, bộ nhớ ngoài, băng thông I/O, điện năng và nhiệt
Nếu định nghĩa lại mọi thứ thành Branch™, thì một số Branch™, bao gồm cả những thứ không phải nhánh thực sự, có thể được tính trước
Nhưng việc loại bỏ nhánh theo cách nói thông thường chẳng phải là xử lý những trường hợp đường tính toán thực sự rẽ nhánh trong mã kiểu if/else sao
Ngay cả trong thế giới đó vẫn có thể có các tối ưu hóa hữu ích, nhưng chúng sẽ bị giới hạn ở các Branch™ cố gắng tính đồng thời nhiều kết quả trong tương lai
Mỗi khi có các phép toán có thể thực hiện độc lập, sẽ xuất hiện khả năng chạy chúng đồng thời
Không chỉ nói về giải mã, nạp lệnh và thực thi
Nếu có ALU và bộ dịch bit độc lập, thì trong lúc cộng cũng có thể dịch bit; nếu có bộ cộng và bộ nhân chuyên dụng, không có lý do gì để không thử làm cả hai cùng lúc
Điều này đồng nghĩa với việc ta sẽ muốn có nhiều lệnh cùng đang được xử lý, và cần có khả năng nạp cũng như giải mã lệnh nhanh hơn tốc độ xử lý
Nó cũng tự nhiên dẫn đến tình huống muốn sắp xếp lại để N lệnh Add không ngăn ta nhìn thấy một lệnh Shift độc lập
Có thể bạn cho rằng cấu trúc hiện nay phức tạp hơn mức cần thiết, và cũng có thể bạn không sai
Nhưng vì có lượng kỹ thuật khổng lồ được đầu tư để tạo ra cấu trúc hiện tại, nên nếu nghĩ rằng có thể làm nhanh hơn nhiều bằng một cách khác, cần đào sâu xem lập luận đó chính xác đến mức nào
Ở đoạn “Ước gì có một ngôn ngữ bare-metal sạch và nhanh để viết tất cả những thứ này…”, tác giả có chèn chú thích “BUT RUST..” và “BUT ZIG..”, nhưng tôi tò mò không biết Nim thì sao
Có vẻ có phần triển khai thư viện native cho
lowerBound: https://github.com/nim-lang/Nim/blob/version-2-0/lib/pure/al...Nói nghiêm ngặt thì nó không phải ngôn ngữ “bare-metal”, nhưng vì biên dịch sang C hoặc C++, nên sẽ thú vị nếu xem nó được biên dịch thành mã gì ở đây
Và tôi cũng tò mò C thì có vấn đề gì
TigerBeetle dùng triển khai không rẽ nhánh riêng: https://github.com/tigerbeetle/tigerbeetle/blob/e996abcf7154...
Những trường hợp như vậy chính là lý do cần template của C++
C không sạch
Tôi vẫn không chắc đây có còn là
lower_boundkhôngCó thể tôi đọc nhầm mã, nhưng có vẻ khi có phần tử trùng lặp, nó trả về một mục khớp bất kỳ chứ không phải mục khớp đầu tiên
Nếu hàm so sánh đang tìm một tiền tố chuỗi cụ thể để phục vụ tự động hoàn thành, thì ngay cả trong danh sách duy nhất cũng có thể có nhiều mục khớp, và khi đó ta muốn mục đầu tiên trong danh sách
Tôi tò mò vì sao bạn lại nghĩ là không
Ước gì mọi bài blog đều bắt đầu như bài này: “Chắc các bạn bận, nên tôi sẽ đi thẳng vào vấn đề. Đây là triển khai tìm kiếm nhị phân C++ nhanh nhất, tổng quát nhất và đơn giản nhất”
Thư viện chuẩn Zig không gọi C++ để tìm kiếm nhị phân
Tìm kiếm nhị phân hiện nằm ở đây: https://github.com/ziglang/zig/blob/b835fd90cef1447904d3b009...
Tôi không hiểu lắm
Vấn đề của tìm kiếm nhị phân và nhánh không phải là bản thân nhánh, mà là trước khi hoàn tất phép so sánh, ta không biết cần lấy vị trí bộ nhớ nào tiếp theo trong mảng
Dù dùng nhánh hay thứ gì khác cũng không quan trọng; cuối cùng vấn đề là ta muốn bộ xử lý làm gì
Có sự phụ thuộc dữ liệu
Trước khi đọc chỉ số giữa, ta không biết sẽ tìm trong nửa trên hay nửa dưới
Có thể suy đoán và phát lệnh đọc cả hai phía; như vậy sẽ giải quyết được phụ thuộc, nhưng lại tăng lưu lượng bộ nhớ
Trọng tâm là liệu đó có phải đánh đổi đúng hay không, còn chỉ loại bỏ nhánh thì không phải câu trả lời
Phần cuối bài có bàn đến: https://mhdm.dev/posts/sb_lower_bound/#prefetching
Vì thế tìm kiếm nhị phân nhanh hơn một cách đúng đắn sẽ dùng bố trí mảng Eytzinger: https://algorithmica.org/en/eytzinger
Trên bộ xử lý Cascade Lake của tôi,
-mllvm -x86-cmov-converter=falselàm hiệu năng tìm kiếm nhị phân giảm gần một nửaCác con số là nanosecond cho mỗi lần bsearch trên mảng
uint32100MBclang 15.0.7 có vẻ kém gcc 13.2.1 hơn rất nhiều trong việc tối ưu hóa đoạn mã cụ thể này
Có thể xem assembly tại đây: https://godbolt.org/z/cbx5Kdjs6
Assembly của gcc trông gọn gàng hơn nhiều
100MB đủ lớn để phiên bản có nhánh hơi có lợi thế, nhưng không phải vì nó tốt hơn, mà là do đặc tính thực thi suy đoán của x86
Có ai biết liên kết “BUT RUST” ban đầu đáng lẽ trỏ tới đâu không?
Vì không cố định phiên bản nên có vẻ nó đã hỏng, và không biết có phải định trỏ vào giữa phần chú thích tài liệu của
starts_withhay khônglet mid = left + size / 2;[1] https://web.archive.org/web/20230602210213/https://doc.rust-...
Vốn định liên kết tới triển khai tìm kiếm nhị phân của Rust
Đã được cập nhật thành https://doc.rust-lang.org/1.71.1/src/core/slice/mod.rs.html#...
Điều thú vị là với hàm so sánh
compphức tạp hơn thì kết quả này không còn giữ nguyênBài viết cho biết họ đã nghĩ đến các kịch bản tìm kiếm nhị phân khá thực tế, trong đó hàm so sánh chậm, như ID, số điện thoại, tài khoản, từ khóa, nên đã thử nghiệm tìm kiếm chuỗi 8 byte
Trong trường hợp này,
std::lower_boundnhanh hơnsb_lower_boundrất nhẹ nhưng nhất quán; để luôn đạt hiệu năng tốt nhất, thư viện nên dùngsb_lower_boundkhi xử lý trực tiếp các kiểu nguyên thủy, còn các trường hợp khác thì dùngstd::lower_boundMuốn xem phần phân tích ở đây
Nếu dữ liệu và đầu vào thật sự ngẫu nhiên, dự đoán sẽ sai khoảng một nửa
Cách dùng CMOV bị chặn sau hàm so sánh do phụ thuộc dữ liệu
Trung bình, cách dùng nhánh thực hiện hai phép so sánh cùng lúc, còn CMOV thực hiện một phép, nên khi thời gian so sánh lớn hơn penalty do dự đoán nhánh sai, có lẽ sẽ xuất hiện điểm đảo chiều
Một bản tôi từng làm qua loa bằng SIMD trước đây nhanh hơn
std::lower_bound3 lần cho đến khi bị giới hạn bởi băng thông bộ nhớ: https://github.com/matthewkolbe/ThinkingInSimd/tree/main/alg...Giả sử là hoàn toàn ngẫu nhiên, nhưng nếu các chuỗi 8 byte này không phải là thông tin thuần túy, bộ dự đoán nhánh hiện đại có thể dễ dàng đạt hiệu năng tốt hơn
cmovCó vẻ thuộc tính
unpredictablegiờ đã ảnh hưởng đến pass chuyển đổi cmovTính đến ngày 1/6, nên có lẽ sẽ được đưa vào clang 17/18: https://reviews.llvm.org/D118118