Điểm:2

Cách hiệu quả để chọn một chỉ mục mảng bằng cách sử dụng một số ngẫu nhiên 64 bit?

lá cờ in

Nói, tôi có uint64_t rand = <số ngẫu nhiên>, và mảng char[20] = .... Mục tiêu của tôi là chọn một phần tử trong mảng dựa vào nội dung của rand.

  1. Một cách chậm là sử dụng phần còn lại: size_t i = rand% 20 sau đó chọn phần tử theo mảng[i].
  2. Một cách khác, mà tôi đoán nhanh hơn, là tôi = rand/UINT64_MAX * 20. Hoặc, để tránh cần các hoạt động thả nổi, bộ đếm nghịch đảo của nó 20/(UINT64_MAX/rand).
  3. Cách thứ 3 là sử dụng các bit ngẫu nhiên để phân nhánh thành chỉ mục giống như một cái cây (nhưng bỏ lỡ mọi số thứ 5):
size_t tổng_byte = 20;
size_t mặt nạ = 1;
size_t tôi = 0;
trong khi (total_byte) {
  if (rand & mask) i += total_bytes / 2; // rẽ phải
  khác tôi += 0; // nhánh trái
  mặt nạ <<= 1;
  tổng_byte /= 2;
}

Có cách nào nhanh hơn trên phần cứng phổ biến không? Ví dụ. máy tính xách tay/máy tính để bàn?

Lý do tôi quan tâm: Tôi đang triển khai chức năng dẫn xuất khóa cứng bộ nhớ và tại một số điểm, tôi cần chọn một phần tử mảng dựa trên nội dung của bản mã được tính toán. Số ngẫu nhiên là 64 bit.

Ngôn ngữ đích là C.

Meir Maor avatar
lá cờ in
Bạn đã thực sự kiểm tra %20 quá chậm chưa? Trên một PC hiện đại? Tôi sẽ bị sốc.
Maarten Bodewes avatar
lá cờ in
@caveman Đừng bận tâm, câu hỏi hơi khác so với dự kiến. Tâm sự đêm khuya....
lá cờ in
Đã đăng chéo: https://stackoverflow.com/questions/68809491/whats-the-fastest-method-in-c-for-converting-a-64bit-random-number-into-a-small với nhiều chi tiết hơn trong phần bình luận , bao gồm cả "20" không phải là hằng số.
Điểm:4
lá cờ ng

đồng % 20 tạo ra một kết quả trong $\{0,1,\ldots,18,19\}$ đó là Gần đồng phục (giả sử rand Là): $\Pr(19)/\Pr(0)=1-1/922337203685477581$. Đó thường là một sự thiên vị chấp nhận được.

Trên "máy tính xách tay/máy tính để bàn" có CPU 64 bit hiện đại, đồng % 20 tương đối nhanh và có những ưu điểm quan trọng là chính xác, đơn giản và dễ thích nghi. Tuy nhiên nó ít nhất là thường xuyên (xem bình luận) có thể nhanh hơn bằng cách sử dụng

(rand-((rand-(rand>>2))>>1))>>59

có cùng tỷ lệ (tối ưu) giữa kết quả ít nhất và có thể xảy ra nhất, trong khi chỉ sử dụng các phép toán thay đổi và thêm. Tôi tự tin hơn rằng mã được tạo là liên tục, điều này có thể quan trọng trong các ứng dụng tiền điện tử. Và giá trị trung bình gần với $19/2$.

Để có trực giác về cách thức hoạt động của công thức đó: đối với bất kỳ $x\in\mathbb R$ nó giữ $(x-(x-x\,2^{-2})\,2^{-1})\,2^{-59}=20\,x\,2^{-64}$, do đó về cơ bản chúng tôi đánh giá những gì các biểu thức (uint64_t)sàn(rand*(20/(UINT64_MAX+1.))) hoặc (uint64_t)((rand*(uint128_t)20)>>64) cố gắng đánh giá. Lưu ý rằng đối với một số giá trị bao gồm rand=0xCCCCCCCCCCCCCCCC công thức sau không hoàn toàn trùng khớp với công thức tôi đề xuất; nhưng phân phối đạt được bởi cả hai là đồng nhất tối ưu.

Phương pháp này không giới hạn ở hằng số $m=20$ cho kích thước mảng. Nó khái quát hóa cho bất kỳ hằng số $m$ với trọng lượng Hamming vừa phải. Tính toán số ca thích hợp từ các hằng số là không cần thiết. tôi đề cập đến điều này câu trả lời tuyệt vời (lưu ý: số ca làm việc cuối cùng được đưa ra phải tăng thêm 32 trong trường hợp hiện tại) đối với thứ gì đó hoạt động, nhưng không phải lúc nào cũng tối ưu. Tôi không có tài liệu tham khảo nào khác cho phương pháp mà tôi (lại-?)đã phát minh ra cho ARM Cortex-M0, nơi nó tỏ ra hữu ích. Trên thực tế, theo kinh nghiệm, tôi chỉ tìm thấy các công thức cho một vài hằng số phù hợp với nhu cầu của mình và Anders Kaseorg hoàn toàn tin tưởng vào cách tạo công thức một cách có hệ thống.


Nếu chúng tôi sẵn sàng mất một chút tính đồng nhất và đảm bảo rằng mã là thời gian không đổi, chúng tôi có thể sử dụng

((rand>>3)*5)>>59

cái nào đơn giản hơn, có khả năng nhanh hơn và dễ dàng thích ứng với các hằng số khác $m$ còn hơn là $20$: chúng tôi viết $m$ như $r\,2^i$ với $i$ một số nguyên và $r$ tốt nhất là lẻ, sau đó tìm số nguyên $j$ với $2^{j-1}\le r<2^j$. Chúng tôi sử dụng ((rand>>j)*r)>>(64+i-j). Vấn đề là, càng thấp $j$ bit của rand không được sử dụng và tính đồng nhất của kết quả bị giảm tương ứng (ngoại trừ nếu $m$ là lũy thừa của hai).

Khi nào $m$$2^j$ cho một số nguyên $j$, chúng ta có thể sử dụng rand>>(64-j) hoặc rand&(m-1). Cái sau được chú ý trong câu trả lời khác. Các phương pháp này không mất tính đồng nhất, nếu tất cả các bit của rand là thống nhất và độc lập.

Nếu $m$ thay đổi trong thời gian chạy với $m<2^j$ cho một số hằng số đã biết $j$, chúng ta có thể sử dụng

((rand>>j)*m)>>(64-j)

tuy nhiên $j$ bit thấp hơn của rand bị mất và điều đó làm giảm tính đồng nhất của kết quả (ngoại trừ nếu $m$ là lũy thừa của hai).


Đề ra:

  • (uint64_t)(sàn(rand*(20/(UINT64_MAX+1.)))) sẽ ổn nếu không có lỗi làm tròn, nhưng vì những lỗi này tồn tại nên khó biết liệu nó có mang lại kết quả hay không 20 cho một số đầu vào; cũng trên nhiều trình biên dịch, nó không thống nhất tối ưu.
  • (uint64_t)((rand*(uint128_t)20)>>64) đúng về mặt toán học và rất gần với những gì chúng tôi đánh giá, nhưng uint128_t là một tính năng C tùy chọn và vẫn được hỗ trợ một chút.
  • câu hỏi là rand/UINT64_MAX * 20 đầu ra trong $\{0,20\}$ như vậy là không phù hợp. Các vấn đề là phép chia làm tròn xuống số nguyên và (độc lập) rằng rand có thể UINT64_MAX.
  • câu hỏi là 20/(UINT64_MAX/rand) đầu ra trong $\{0,1,2,3,4,5,6,10,20\}$ và có thể gây ra phép chia cho 0, do đó không phù hợp. Các vấn đề là phép chia làm tròn xuống số nguyên và (độc lập) rằng rand có thể 0.
  • Đoạn mã 3 của câu hỏi luôn có i%5 != 4 trên đầu ra, do đó là không phù hợp. Vấn đề là đầu ra tôi được xây dựng như 10+5+2+1 với một số điều khoản bị loại bỏ.
Gilles 'SO- stop being evil' avatar
lá cờ cn
Khi tối ưu hóa tốc độ trên CPU 64 bit điển hình, phần dư hoặc phép chia cho một hằng số được biên dịch thành phép nhân với một hằng số cộng với một số ca và cộng/trừ. Phân chia phần cứng chậm và trình biên dịch biết điều đó (mặc dù hầu hết sẽ không thực hiện phép toán thời gian biên dịch cho phân chia 64 bit trên CPU 32 bit).Các ca mà bạn đề xuất có cùng số lượng lệnh, nhưng không có phép nhân và cùng số lần truy cập bộ nhớ, vì vậy phương pháp thay đổi của bạn rất có thể sẽ nhanh hơn trên bất kỳ CPU nào ngoại trừ một số được thiết kế cho thời gian thực với số lượng chu kỳ thấp. /div. https://godbolt.org/z/z4PverffY
fgrieu avatar
lá cờ ng
@Gilles'SO-stop beingevil': Tôi không tìm thấy thông tin thích hợp trong [mớ hỗn độn đó](https://software.intel.com/content/dam/develop/external/us/en/documents-tps/325462-sdm -vol-1-2abcd-3abcd.pdf) để xác nhận rằng tính năng tối ưu hóa mà bạn đề cập vẫn có giá trị trên các CPU x64 mới nhất. Cập nhật: Tôi đã chỉ cho [những](https://www.agner.org/optimize/#manuals) tài nguyên hữu ích này.
Gilles 'SO- stop being evil' avatar
lá cờ cn
Tôi nghĩ bạn cần tìm một hướng dẫn dành riêng cho kiểu máy cho điều đó. Bạn đã liên kết với tài liệu tham khảo kiến ​​trúc chung. Tham chiếu tập lệnh (tập 2) sẽ phù hợp hơn, nhưng thậm chí đó chỉ là mô tả chức năng, nó không bao gồm số chu kỳ (không kể toàn bộ câu chuyện về hiệu suất, nhưng đối với trường hợp đơn giản này, không có phân nhánh hoặc song song vì vậy tôi nghĩ rằng việc thêm số chu kỳ sẽ dẫn đến một phép so sánh có ý nghĩa).
caveman avatar
lá cờ in
Có đáng để tổng quát hóa giải pháp dịch chuyển đó sang bất kỳ số nào khác ngoài 20 để đạt được ít chu kỳ hơn so với sử dụng phương pháp `%` không? Bởi vì 20 không phải là một hằng số, mà chỉ là một ví dụ mà tôi đã chọn.
fgrieu avatar
lá cờ ng
@caveman: câu trả lời bây giờ làm rõ rằng có, chúng ta có thể mở rộng sang các hằng số khác. [Điều này](https://tinyurl.com/unicst) cung cấp công thức cho tất cả các hằng số có tối đa 3 chữ số thập phân (nhưng hãy nhớ thêm 32 vào số lần thay đổi cuối cùng). Một lần nữa, sự tối ưu hóa đó chỉ có ý nghĩa nếu toán tử `%` chậm và nó sẽ không có trên máy tính xách tay/máy tính để bàn hiện đại.
Gilles 'SO- stop being evil' avatar
lá cờ cn
@caveman Tôi không phải là chuyên gia nhưng tôi nghĩ rằng về mặt hiệu suất, các phép tính cần thiết để tính toán các ca cần thiết sẽ tốn nhiều hơn một lệnh chia. Tuy nhiên, phương pháp thay đổi có những lợi ích khác ngoài hiệu suất, chủ yếu là được đảm bảo không có thời gian phụ thuộc vào dữ liệu bí mật.
lá cờ pe
Đây có vẻ là một phiên bản phức tạp hơn của [Lemire](https://lemire.me/blog/2016/06/27/a-fast-alternative-to-the-modulo-reduction/) `(rand() * 20) >> Cách tiếp cận 64`.
fgrieu avatar
lá cờ ng
@SamuelNeves: có sự khác biệt. (A) Biểu thức `(rand() * 20) >> 64` cần sản phẩm được đánh giá trên 69 bit và điều đó không thể thực hiện được; thủ thuật Lemire được liên kết là với `rand()` 32 bit được mở rộng thành 64 bit và chạm vào bức tường đó để có `rand()` 64 bit. (B) Đối với một số giá trị của `rand()` bao gồm 0xCCCCCCCCCCCCCCCC, những gì tôi đề xuất khác đi một, nhưng vẫn có phân phối thống nhất lý tưởng.
Điểm:3
lá cờ in

Chỉ cần làm% 20

Dựa theo http://ithar.com/infographics-operation-costs-in-cpu-clock-cycles/ Phân chia số nguyên không tốn 12-44 chu kỳ cpu trên CPU hiện đại (và trong một số trường hợp ít hơn do cấu trúc đường ống nếu ALU không làm gì khác) Xem xét điều tiếp theo bạn muốn làm là truy cập bộ nhớ mà tốt nhất sẽ là lần đọc L1 sẽ tự tiêu tốn 3-4 chu kỳ và có thể bạn muốn làm điều gì đó với giá trị này.

Tôi không thể tưởng tượng được một kịch bản mà điều này đáng để tối ưu hóa ngay cả khi có thể giảm một hoặc hai tích tắc đồng hồ.

Tìm kiếm các nút thắt cổ chai trước khi tối ưu hóa.

fgrieu avatar
lá cờ ng
[Hình ảnh](http://ithare.com/wp-content/uploads/part101_infographics_v08.png) trong nguồn hữu ích của bạn nói rằng phép chia số nguyên tốn 15-40 chu kỳ. Văn bản đã trích dẫn một tài liệu tham khảo là đưa ra "chi phí phân chia 32/64 bit (được gọi là DIV/IDIV trên x86/64) - ở giữa 12-44 chu kỳ". Theo kinh nghiệm của tôi, điều đó cực kỳ phụ thuộc vào nền tảng và độ rộng của các đối số, và trực giác của tôi là 15 hoặc thậm chí 12 không phản ánh lợi thế của năm 2021. Trực giác ban đầu (được chia sẻ) của chúng tôi rằng trên CPU x64 `i%20` là đủ nhanh và có thể là nhanh nhất vẫn có ý nghĩa.
Meir Maor avatar
lá cờ in
@fgrieu Đúng là mình chép nhầm số, mình sửa lại số rồi. Nó không thay đổi điểm mấu chốt. Điều này là nhanh chóng.
Gilles 'SO- stop being evil' avatar
lá cờ cn
Nếu 20 là một hằng số và các số không lớn hơn một từ máy, thì `% 20` thường sẽ được tối ưu hóa thành phép nhân, quá trình này mất ít chu kỳ hơn phép chia, giúp giảm thêm sự khác biệt. Trong mọi trường hợp, tôi đồng ý rằng ngay cả sự phân chia cũng không đáng kể so với truy cập bộ nhớ trên bất kỳ nền tảng nào có bộ nhớ cache (đặc biệt nếu đó là tra cứu bảng thời gian không đổi yêu cầu nhiều lần tải). Tuy nhiên, đối với các ứng dụng mật mã, việc sử dụng phép chia hoặc phép nhân có thể không được mong muốn vì chúng thường có thời gian phụ thuộc vào dữ liệu.
Meir Maor avatar
lá cờ in
Ban đầu, tôi đưa ra số chu kỳ cho phép nhân và sau đó chỉnh sửa nhận xét sau. Tối ưu hóa vi mô thực tế như thế này rất phức tạp và phụ thuộc vào những gì khác đang diễn ra để xem cpu đóng gói các hướng dẫn tốt như thế nào. Mặc dù tôi nghĩ rằng tôi sẽ không đưa ra câu trả lời của mình lâu hơn nó.
Điểm:1
lá cờ sk

Thông thường, người ta sẽ cố gắng làm cho kích thước mảng có lũy thừa bằng 2. Sau đó, chỉ số có thể được tính theo bit AND:

mảng ký tự [0x40];
uint64_t rand;
...
char c = mảng[rand & 0x3f];
lá cờ id
Đó là câu trả lời "Tôi có thể giải một bài toán khác rất nhanh". Chắc chắn, nhưng đó không phải là câu hỏi đang được hỏi. Và trong tiền điện tử, khi thuật toán yêu cầu sử dụng 20, bạn không thay thế 32 chỉ vì điều đó sẽ nhanh hơn. Kiểu lập trình đó là cách bạn phá vỡ tiền điện tử.
ThomasM avatar
lá cờ sk
Khi tôi hiểu câu hỏi, thuật toán không được đưa ra nhưng đang được xây dựng. Mặt khác, có thể sẽ có một cách xác định cách tính chỉ số từ số ngẫu nhiên và người ta không thể thử các phương pháp khác nhau để tìm ra phương pháp nhanh nhất.

Đăng câu trả lời

Hầu hết mọi người không hiểu rằng việc đặt nhiều câu hỏi sẽ mở ra cơ hội học hỏi và cải thiện mối quan hệ giữa các cá nhân. Ví dụ, trong các nghiên cứu của Alison, mặc dù mọi người có thể nhớ chính xác có bao nhiêu câu hỏi đã được đặt ra trong các cuộc trò chuyện của họ, nhưng họ không trực giác nhận ra mối liên hệ giữa câu hỏi và sự yêu thích. Qua bốn nghiên cứu, trong đó những người tham gia tự tham gia vào các cuộc trò chuyện hoặc đọc bản ghi lại các cuộc trò chuyện của người khác, mọi người có xu hướng không nhận ra rằng việc đặt câu hỏi sẽ ảnh hưởng—hoặc đã ảnh hưởng—mức độ thân thiện giữa những người đối thoại.