1 điểm bởi GN⁺ 2024-07-30 | 1 bình luận | Chia sẻ qua WhatsApp
  • Đây là một thử nghiệm xử lý theo từng 64 byte bằng AVX-512-BW cho tác vụ sao chép chuỗi đồng thời chuyển chữ hoa ASCII sang chữ thường, nhằm khai thác hiệu năng SIMD ngay cả với chuỗi nhỏ
  • Trọng tâm của phần triển khai là so sánh từng byte xem có nằm trong khoảng từ 'A' đến 'Z' hay không, rồi thực hiện phép toán mask cộng thêm 'a' - 'A' chỉ tại các vị trí tương ứng
  • Phần đuôi còn lại của chuỗi ngắn và chuỗi dài được xử lý bằng masked load/store, giúp giảm chi phí xử lý các mẩu nhỏ mà mã SIMD thường gặp
  • Kết quả đo với bản sao khoảng 1MiB theo các chunk từ 1 byte đến 1KiB trên Clang 16, Debian 11, AMD Ryzen 9 7950X cho thấy tolower64 luôn thuộc nhóm nhanh trong các đối tượng so sánh
  • Trên Zen 4, AVX-512-BW tỏ ra rất phù hợp cho xử lý chuỗi, nhưng ARM SVE và RISC-V Vector extension chưa được kiểm chứng trực tiếp một cách chi tiết

Tạo tolower() 64 byte bằng AVX-512-BW

  • Mục tiêu là triển khai một kernel tolower() bằng SIMD để sao chép chuỗi đồng thời chuyển các ký tự ASCII viết hoa sang viết thường
  • AVX-512-BW là phần mở rộng hỗ trợ phép toán theo byte và word, hiện có thể dùng trên các bộ xử lý AMD Zen gần đây
    • AVX-512 được chia thành nhiều phần mở rộng nên việc hỗ trợ khá phức tạp
    • Hỗ trợ phía Intel được đánh giá là đặc biệt thiếu nhất quán
  • ARM SVE cũng cung cấp masked load/store theo byte rất phù hợp cho xử lý chuỗi
    • Có trên các lõi big-ARM Neoverse gần đây, ví dụ Amazon Graviton
    • Không thể dùng trên Apple Silicon
  • RISC-V Vector extension cũng có phong cách tương tự ARM SVE và có thể dùng trên nhiều máy tính bo mạch đơn cỡ nhỏ

Cách tolower64() hoạt động

  • tolower64() là một kernel dựa trên AVX-512 xử lý 64 byte mỗi lần
  • Trước tiên, nạp các giá trị chuẩn vào thanh ghi vector chứa 64 byte
    • 'A'
    • 'Z'
    • 'a' - 'A'
  • So sánh vector ký tự đầu vào c với 'A''Z' để tạo hai mask 64 bit tương ứng
    • các vị trí mà c >= 'A'
    • các vị trí mà c <= 'Z'
  • Kết hợp hai mask bằng _kand_mask64() để tạo mask is_upper chỉ đánh dấu các vị trí chữ hoa
  • Cuối cùng áp dụng _mm512_mask_add_epi8()
    • các byte có is_upper là false sẽ giữ nguyên c
    • các byte có is_upper là true sẽ thành c + ('a' - 'A')

Xử lý chuỗi dài và chuỗi ngắn

  • Phần lớn chuỗi dài được xử lý bằng vector load/store không căn chỉnh thông thường
    • _mm512_loadu_epi8()
    • tolower64()
    • _mm512_storeu_epi8()
  • Các chuỗi ngắn và phần đuôi còn lại của chuỗi dài dùng masked unaligned load/store
  • Mask được tạo sao cho chỉ len bit thấp là bật
    • uint64_t len_bits = (~0ULL) >> (64 - len)
    • đưa vào thanh ghi mask SIMD bằng _cvtu64_mask64(len_bits)
  • _mm512_maskz_loadu_epi8() sẽ điền 0 vào các vị trí trong thanh ghi đích nơi mask bị tắt
  • _mm512_mask_storeu_epi8() chỉ ghi các vị trí có mask bật
  • Đây là chìa khóa để xử lý nhanh các mẩu chuỗi nhỏ

Điều kiện benchmark và các đối tượng so sánh

  • Benchmark được chạy trên Clang 16, Debian 11 và AMD Ryzen 9 7950X
  • Khối lượng đo là khoảng bản sao 1MiB, với độ dài chunk thay đổi từ 1 byte đến 1KiB
  • Để phản ánh chênh lệch căn chỉnh giữa chuỗi nguồn và đích, có chèn vài byte giữa các chuỗi này, và các byte đó không được tính vào lượng 1MiB đo đạc
  • L2 cache của Ryzen 9 7950X là 1MiB mỗi lõi nên mỗi lần chạy thử được kỳ vọng sẽ tràn sang cả L3 cache
  • Mỗi hàm được biên dịch riêng để tránh nhiễu do inline và code motion
    • Trong mã thực tế, việc khuyến khích inline có lẽ phổ biến hơn là ngăn nó

Kết quả: hiệu năng mượt của tolower64

  • Màu hồng, tolower64, nhìn chung luôn nằm trong nhóm nhanh nhất trong các hàm được thử
    • Khi độ dài là 65 byte, hiệu năng giảm nhẹ do chuyển sang vector thứ hai
    • Đường hiệu năng tăng nhanh và không có hố sâu rõ rệt, cho thấy masked load/store rất hiệu quả khi xử lý các mẩu chuỗi ngắn
  • Màu xanh lá, copybytes64, là phiên bản memcpy dùng AVX-512 theo cách tương tự
    • Không nhanh hơn tolower64 quá nhiều
    • Clang mới nhận ra ngữ nghĩa của hàm này và viết lại hoàn toàn, nên nó được biên dịch bằng Clang 11
  • Màu cam, copybytes1, là phiên bản memcpy theo từng byte
    • Được biên dịch bằng Clang 11
    • Cho thấy heuristic tự động vector hóa của Clang 11 tương đối kém với các mẩu chuỗi nhỏ hơn 256 byte
  • Màu đỏ, tolower, là baseline gọi tolower() chuẩn của <ctype.h> và rất chậm
  • Màu tím, tolower1, là tolower() theo từng byte được biên dịch bằng Clang 16
    • Tự động vector hóa của Clang 16 đã tốt hơn nhiều so với Clang 11
    • Vẫn chậm hơn phiên bản viết tay và sinh ra mã phức tạp hơn nhiều
    • Khả năng xử lý mẩu chuỗi ngắn không tốt bằng tolower64, nên đồ thị hiệu năng dao động nhọn
  • Màu nâu, tolower8, là tolower() SWAR từ bài trước
    • Clang có thử tự động vector hóa nhưng hàm quá phức tạp nên kết quả không tốt
    • Dù được biên dịch bằng Clang 16, nó vẫn xuất hiện vách hiệu năng kiểu Clang 11 ở mốc 256 byte
  • Màu xanh dương, memcpy, gọi memcpy của glibc
    • Ban đầu nhanh nhưng có đoạn giảm xuống còn khoảng một nửa tốc độ của copybytes64
    • Nguyên nhân vẫn chưa được xác định

Kết luận và mã nguồn

  • AVX-512-BW rất phù hợp để xử lý chuỗi, đặc biệt là chuỗi ngắn
  • Trên Zen 4 nó rất nhanh, và các hàm intrinsic cũng tương đối dễ dùng
  • Đặc điểm nổi bật nhất là hiệu năng mượt
    • Hầu như không thấy các hố hiệu năng do tự động vector hóa chuyển sang mã scalar khi gặp mẩu chuỗi nhỏ
  • Do không thể tiếp cận thuận tiện thiết bị hỗ trợ ARM SVE hay RISC-V Vector extension, tác giả chưa thể khảo sát chi tiết hai phần mở rộng này
  • Có thể xem mã tại kho git trên trang web

1 bình luận

 
GN⁺ 2024-07-30
Các ý kiến trên Hacker News
  • Thủ thuật “unsafe read beyond of death” dù được phần cứng cho phép, vẫn bị xem là hành vi không xác định trong mô hình bộ nhớ của Rust và LLVM
    Cũng như các hành vi không xác định khác, trình biên dịch có thể giả định trong quá trình tối ưu hóa rằng “chuyện đó không xảy ra”, dẫn đến kết quả ngoài dự kiến; muốn né thì phải dùng inline assembly
    https://github.com/ogxd/gxhash/issues/82

    • Sẽ tốt hơn nếu có một lựa chọn không phải assembly cho những trường hợp như thế này
      Có vẻ không quá khó để hỗ trợ kiểu load “các giá trị vượt ngoài phạm vi cấp phát được đọc như phần tử không xác định, và chỉ là hành vi không xác định khi phần cứng không thích”; thậm chí bên trong chỉ là bí danh của lệnh assembly tương ứng cũng đủ
      Xa hơn nữa, sẽ tốt nếu sau mọi vùng cấp phát như malloc, stack, hằng số, v.v. đều được bảo đảm có tối thiểu khoảng 64 byte địa chỉ không gây fault, nhưng việc này phức tạp hơn nhiều vì cần nhiều thành phần phối hợp
      Với allocator tùy chỉnh thì chuyện này không đáng kể, nhưng khi đó khó dùng mã SIMD cho dữ liệu nằm ngoài heap tùy chỉnh, và vẫn bị ràng buộc bởi một khả năng segfault rất nhỏ
      Sanitizer hay Valgrind vẫn hữu ích. Vì các giá trị vượt phạm vi có thể được theo dõi như giá trị chưa xác định, và có thể báo lỗi khi thực sự được sử dụng
    • Ngay cả ở mức phần cứng, tôi cũng nghi ngờ liệu điều đó có thật sự đúng không
      Tôi tò mò điều gì sẽ xảy ra nếu đọc vào trang chưa được ánh xạ hoặc vùng nhớ được bảo vệ, và vì chưa xem mã nên không rõ bảo đảm căn chỉnh có tránh được việc này hay không
    • Cách giải thích “nếu là hành vi không xác định thì trình biên dịch có thể giả định chuyện đó không xảy ra” là sai
      Hành vi không xác định là thuật ngữ chuyên môn của chuẩn C nên việc khái quát hóa như vậy tự nó đã kỳ lạ; ANSI C không cho phép rõ ràng giả định đó, còn ISO C tuy mở hơn nhưng cũng không biện minh cụ thể cho giả định ấy
      Tôi cho rằng kiểu giải thích “UB = có thể giả định là không thể xảy ra” gần như là hù dọa khá thiếu trung thực
  • Nhìn đoạn mã gọn gàng và hiệu năng cao trong bài, tôi tò mò triển khai AVX512 của AMDAVX10 sắp tới của Intel sẽ cạnh tranh ra sao
    Trọng tâm của AVX10 có vẻ là giải quyết tình trạng P-core/E-core của Intel, còn AMD dường như chọn cách tiếp cận tốt hơn: tùy trường hợp dùng triển khai toàn bề rộng trên Zen5 hoặc xử lý 256-bit hai lần trên Zen4 và Zen5 mobile, trong khi vẫn giữ API mượt mà
    Mức tăng hiệu năng lớn trong bài cũng đều là kết quả từ nhân Zen4, và AVX512 có rất nhiều ưu điểm, nên thật bực khi Intel đã hạn chế nó quá mức để phân khúc thị trường, trên thực tế ngăn cản việc áp dụng trong mã client phổ thông

    • Nếu Intel thực sự đưa AVX10/256 vào tất cả CPU ra mắt trong tương lai, cuối cùng họ sẽ thắng nhờ độ phổ biến
      Thị trường đã nhiều lần từ chối việc rẽ nhánh đường đi mã theo từng CPU, và các triển khai SIMD thật sự quan trọng thường là mẫu số chung thấp nhất
      AVX10.1/256 và AVX512VL có một tập con chung, nên khi đủ thời gian trôi qua và phần lớn CPU đều hỗ trợ, mọi người sẽ nhắm tới phần đó
      AMD sẽ tiếp tục có những chiến thắng dễ dàng trong một số ứng dụng benchmark được cập nhật để hỗ trợ AVX512, nhưng nếu Intel giữ kế hoạch AVX10, cuối cùng AMD nhiều khả năng cũng sẽ dùng rộng rãi pipeline SIMD xử lý hai lần để hỗ trợ AVX10/256 hiệu quả trong khi vẫn duy trì tương thích AVX512
      Intel đã có rất nhiều lựa chọn tệ trong 10 năm qua, nhưng chia cắt thị trường bằng tập lệnh là một trong những điều tệ nhất. Họ đã tự giết động lực và sự quan tâm dành cho các đổi mới mới nhất; các tính năng như thao tác mask quan trọng hơn nhiều so với độ rộng, nên tôi mong họ đưa AVX10/256 vào toàn bộ dòng sản phẩm
    • Triển khai AVX512 của Zen 4 không phải là double-pumped, và các phóng viên công nghệ nên ngừng gọi nó như vậy
      Cụm từ đó có một ý nghĩa cụ thể, nhưng không khớp với cách hoạt động thực tế
      Zen 4 chỉ giải mã các phép toán trên thanh ghi ZMM thành nhiều vi thao tác rồi lập lịch chúng lên các đơn vị 256-bit đang rảnh; riêng shuffle toàn bề rộng 512-bit được xử lý đặc biệt bằng phần cứng chuyên dụng để tránh mô phỏng đắt đỏ
      Vì vậy Zen 4, với 4 đơn vị SIMD 256-bit, vẫn hoạt động như một nhân 2×512-bit mạnh mẽ; triển khai này hoàn toàn không phải cách làm rẻ tiền, và có khả năng là hình thái tốt nhất từng có trên phần cứng tiêu dùng cho đến nay
    • Tôi không hiểu vì sao Intel không giải quyết vấn đề này bằng cách đưa AVX512 xử lý hai lần vào E-core. Hoặc đơn giản là làm CPU desktop chỉ có P-core, như lẽ ra phải vậy
      Họ đã có nhiều năm để sửa, và thật khó chịu khi dù AMD hỗ trợ thì vẫn không được thị trường đón nhận vì thị phần; đáng tiếc là AVX10 có vẻ sẽ giúp Intel níu giữ thế giới lâu hơn
      Trên desktop, tôi muốn thấy các nhân tốt hơn, nhiều nhân hơn, và một tập lệnh được chuẩn hóa tốt mở ra các tính năng hữu ích như SIMD rộng, float16, gather/scatter; AMD đang làm khá tốt
      Trong khi đó Intel lại đặt các nhân yếu cạnh các nhân khá tốt, giới hạn các nhân tốt để phù hợp với nhân yếu, ra mắt CPU có cùng số nhân qua nhiều thế hệ, dùng nhân yếu để làm như thể số nhân nhiều hơn, tung ra quá nhiều biến thể tập lệnh đến mức khó hình thành một tập chung hữu ích, và còn bỏ cả hỗ trợ tập lệnh mà họ từng có vẻ đã hứa hẹn
      Sở thích của các hãng sản xuất desktop từng là Intel ở thập niên 90, AMD đầu những năm 2000, Intel cuối những năm 2000 và thập niên 2010, còn giờ lại là AMD. Tôi tò mò Intel sẽ làm gì để lấy lại nền tảng ngoài việc cản trở đối thủ, và cạnh tranh phải tiếp tục để một bên không trở nên quá tự mãn
  • Tài liệu đọc cho vui: http://www.unicode.org/Public/3.1-Update1/CaseFolding-4.txt

    • Cũng có chuyện như thế này: nếu đổi chữ ß trong tiếng Đức sang chữ hoa thì độ dài chuỗi sẽ thay đổi
      Ví dụ "straße".upper() sẽ thành 'STRASSE'
      Ngoài ra, nếu không chỉ định locale, khi chuyển qua lại chữ i không dấu chấm của nhóm ngôn ngữ Turkic giữa chữ hoa/chữ thường, 'ı'.upper().lower() sẽ thành 'i' và bị hỏng
    • May là đoạn code này xuất phát từ xử lý DNS nên chỉ dùng ASCII, không cần xử lý những phức tạp đó
      Có nhiều giao thức ASCII không phân biệt hoa/thường, và chúng thường xuất hiện trên hot path của nhiều server
    • Chuỗi dùng nội bộ như ID khác với văn bản do con người nhập
      Trường hợp trước thường chỉ cần ASCII thuần với mã hóa 8-bit là đủ, nhưng trường hợp sau thì phức tạp hơn
      Địa chỉ DNS là ví dụ dễ hiểu: về mặt kỹ thuật có thể chứa gần như mọi Unicode, nhưng khi phân giải DNS thực tế nó được chuyển thành một tập con ASCII rất hạn chế, và quá trình phân giải đó không phân biệt chữ hoa/thường
      Tất nhiên cũng có những ngôn ngữ lập trình hỗ trợ toàn bộ hệ chữ của Unicode nhưng identifier lại không phân biệt hoa/thường. Nếu bạn đang phải xử lý thứ như vậy thì xin chia buồn
    • Liên quan đến ví dụ tiếng Đức maße đổi thành MASSE, trong tiếng Đức còn có chữ Eszett viết hoa là
      Nó vẫn chưa được triển khai rộng rãi và cũng ít font hỗ trợ, nhưng về mặt lý thuyết thì giờ đã tồn tại
  • Tôi thấy phần giải thích “cộng mask” trong bài có vẻ sai
    Chẳng phải khi is_upper là false thì mới cộng, còn true thì sao chép nguyên trạng sao?

    • À, tôi muộn màng nhận ra tên biến to_upper bị đặt ngược, đáng ra nên gọi là to_lower
      Cảm ơn đã chỉ ra chỗ gây nhầm lẫn; tôi đã sửa bài và code
    • Phép toán này là tolower
      Chữ hoa A là 0x40, chữ thường là 0x60, nên việc cộng 0x20 phải xảy ra khi is_upper là true
  • Các kiểu tối ưu hóa SWAR như vậy thường chỉ hữu ích khi chuỗi được căn chỉnh theo địa chỉ 8 byte
    Áp dụng thuật toán SWAR cho chuỗi không được căn chỉnh thường sẽ chậm hơn thuật toán gốc
    Nếu tách thành 3 bước: xử lý phần đầu cho tới địa chỉ đã căn chỉnh, xử lý phần thân đã căn chỉnh, rồi xử lý phần đuôi dưới 8 byte, thì số lệnh sẽ tăng thêm
    Có một trường hợp và benchmark tương tự với tuyên bố sai rằng utf8.IsValid trong Go nhanh hơn tại đây: https://github.com/sugawarayuuta/charcoal/pull/1

    • Các phép toán SIMD có mask của AVX-512 và ARM SVE được tạo ra để giải quyết vấn đề đó
      Phép toán bộ nhớ luôn được căn chỉnh và dùng toàn bộ kích thước vector, nhưng có thể áp mask chỉ cho các phần tử hợp lệ
      Ngay cả khi phép toán bộ nhớ vector có mask không được căn chỉnh và bắc qua một page chưa được ánh xạ hoặc được bảo vệ, nếu lane tương ứng bị tắt bằng mask thì sẽ không phát sinh fault
      Với các phép toán như strlen() khi chưa biết trước độ dài, cũng có lệnh load đặc biệt để rút ngắn độ dài vector ngay trước phần tử đầu tiên sẽ gây fault
  • Phép cộng bằng mask trông khá hay. Sẽ thật tốt nếu trong .NET intrinsic có thể thao tác trực tiếp các thanh ghi mask của AVX512, nhưng hiện tại phải dựa vào “idiom được nhận diện”
    Nếu phân tích vòng lặp lõi do tác giả tạo bằng GCC với uiCA(CQA/MAQAO) trên Ice Lake thì ra khoảng 32B/cycle; quy đổi ở 3GHz, nếu giả định không có nút thắt bộ nhớ, gần như là 96GiB/s. Tất nhiên với kiểu thuật toán này thì truy cập bộ nhớ lúc nào cũng là nút thắt
    Tuy vậy có vẻ vẫn chưa thật sát mức tận dụng tối ưu; dùng Clang thì nhờ kết quả unroll được bung tốt hơn và chọn lệnh tốt hơn, có thể đạt tới 42.67B/cycle. Có lẽ cả cache L2 cũng khó duy trì thông lượng như vậy, nhưng cũng thú vị khi việc chuyển hoa/thường cho chuỗi độ dài trung bình có thể xong trong khoảng thời gian ánh sáng màn hình chạm tới giác mạc
    Vài tháng trước tôi đã triển khai một phép chuyển đổi chữ hoa/thường ASCII trong UTF-8 tương tự bằng C#: https://github.com/U8String/U8String/blob/main/Sources/U8Str...
    Vì chuỗi ngắn chiếm phần lớn trong hầu hết codebase, việc chuyển đổi unroll cho độ dài dưới mức vector hóa là quan trọng, và switch được biên dịch thành jump table cùng branchless fall-through
    Hiện tại tôi chỉ dùng tới 256-bit, vì trên các máy như Zen 3 hoặc 4, vốn chỉ có đơn vị SIMD 256×4, như vậy đã bão hòa rồi. Có ví dụ so sánh cạnh nhau với bản C ở đây: https://godbolt.org/z/eTGYhTPan
    Trên AVX512 có vẻ cũng có thể dùng vpternlogd để chuyển đổi bằng 3 lệnh; khi còn dùng được phần cứng AVX512, .NET đã tối ưu như vậy với độ rộng 256-bit + AVX512VL, nhưng giờ kỳ lạ là không tái hiện được với độ rộng 512-bit
    Chắc cũng sẽ thấy thử nghiệm SWAR thất bại ở phần dispatch bằng switch; tôi thắc mắc giấy phép của bài viết là gì. Nếu vượt qua test suite thì tôi muốn mang về dùng

    • Clang và GCC có cách xử lý intrinsic khác nhau, đặc biệt với lệnh AVX-512, Clang có khả năng lệch khỏi opcode và thuật toán được chỉ định trong hướng dẫn của Intel nhiều hơn GCC
      Nghĩ về cấu trúc của hai compiler thì có thể hiểu được, nhưng kết quả có lúc tốt hơn, có lúc lại thiệt hơn
      Vài năm trước, khi làm một dự án vector hóa cao bắt buộc phải biên dịch được bằng cả hai, cuối cùng tôi phải giữ trong repository cả bản tham chiếu C lẫn inline assembly cho một số target cụ thể và các file .S
      Makefile trở nên lộn xộn, còn test suite thì phải đưa cả benchmark vào, khiến gánh nặng bảo trì rất lớn; vì vậy tôi đi đến kết luận rằng việc dùng intrinsic như một phương tiện cấp thấp tốt hơn auto-vectorization cần phải cực kỳ thận trọng
      Ví dụ: ở https://godbolt.org/z/T4Pjhrz5d output của GCC đúng như dự đoán, nhưng output của Clang thì gây bất ngờ và thực tế chậm hơn. Nếu chạy trong vòng lặp, theo uiCA là 7 chu kỳ so với 4 chu kỳ của GCC, và điều này cũng lộ rõ trong benchmark của ứng dụng thực tế nơi hàm này chạy hàng tỷ lần trong một thuật toán brute-force
      Tôi nhớ khi nhìn vào codebase LLVM cũng từng thấy vấn đề rằng Clang 16 có thể hoàn toàn không phát ra một số lệnh AVX-512 dạng mask do tái cấu trúc nội bộ
    • Phân tích rất hữu ích
      Tôi không nhắm tới hiệu năng tối đa có thể; ban đầu chỉ muốn xem có chạy được không, và việc lần thử đầu tiên ra khá tốt là phần thưởng thêm
      Mối quan tâm chính là các chuỗi ngắn hơn thanh ghi vector và loại bỏ các vùng trũng trên biểu đồ thông lượng
      Nếu theo liên kết tới mã ở cuối bài blog thì có thông tin giấy phép; trừ phần MPL-2.0 vốn viết cho BIND, còn lại là 0BSD hoặc MIT-0
    • Chỉ nhìn vào khối assembly lớn thì khó thấy, nhưng Clang viết lại (x >= 'a' && x <= 'z') thành dạng (x - 'a') < ..., nhờ đó bớt được một lệnh
      Do cách mã hóa opcode kỳ lạ, đôi khi cả thao tác load thanh ghi cũng giảm
  • Tôi không biết swar là gì

    • Đó là viết tắt của “SIMD Within A Register
      Thường chỉ kỹ thuật pack nhiều phần tử vào một thanh ghi và dùng nó gần như SIMD mà không cần lệnh SIMD tường minh
      Ví dụ, nếu đưa các số 31-bit và 32-bit vào một thanh ghi 64-bit rồi chừa 1 bit cho carry, ta có thể thực hiện hai phép cộng chỉ bằng một phép cộng 64-bit
      Trong game, người ta đã pack các giá trị RGB(A) vào số nguyên 32-bit để dùng các thủ thuật như vậy cho đồ họa, và ScummVM cũng có đoạn mã nội suy 2 pixel RGB 16-bit trong một giá trị 32-bit, tổng cộng 6 thành phần: https://github.com/scummvm/scummvm/blob/master/graphics/scal...
    • Nghĩa là SIMD bên trong một thanh ghi
  • Sau khi Unicode xuất hiện, khái niệm chữ hoa và chữ thường đã trở thành một vũng lầy
    Muốn làm cho đúng thì cần rất nhiều dữ liệu
    Nếu bạn đang làm một tác vụ mà chuyện có hoàn thành kịp hay không phụ thuộc vào tốc độ chạy ASCII tolower, thì có lẽ tốt hơn là nên thay đổi cuộc chơi và thay đổi các điều kiện tiên quyết

  • Trước đây tôi từng thêm viền đen quanh ảnh để tránh hoàn toàn vấn đề SIMD đọc vượt quá buffer
    Cách đó hoạt động rất tốt và về tốc độ có thể thắng một số triển khai OpenCV, nhưng không phải lúc nào ta cũng kiểm soát hoàn toàn được input như vậy

  • Tôi tò mò không biết đã thử kiểu này chưa. Kết quả auto-vectorization trông khá gọn
    https://godbolt.org/z/1c5joKK5n

    • Cái đó về cơ bản giống tolower1. Xem các bullet bên dưới biểu đồ là được