Tôi sẽ đi ngược lại với sự khôn ngoan chung ở đây std::copy
sẽ có một sự mất mát hiệu suất nhẹ, gần như không thể nhận ra. Tôi vừa làm một bài kiểm tra và thấy rằng điều đó không đúng: Tôi đã nhận thấy sự khác biệt về hiệu suất. Tuy nhiên, người chiến thắng là std::copy
.
Tôi đã viết một triển khai C ++ SHA-2. Trong thử nghiệm của mình, tôi băm 5 chuỗi bằng cả bốn phiên bản SHA-2 (224, 256, 384, 512) và tôi lặp 300 lần. Tôi đo thời gian bằng Boost.timer. Bộ đếm vòng lặp 300 đó là đủ để ổn định hoàn toàn kết quả của tôi. Tôi đã chạy thử nghiệm 5 lần mỗi lần, xen kẽ giữa memcpy
phiên bản và std::copy
phiên bản. Mã của tôi tận dụng việc lấy dữ liệu càng nhiều khối càng tốt (nhiều triển khai khác hoạt động với char
/ char *
, trong khi tôi hoạt động với T
/ T *
(trong đó T
loại lớn nhất trong triển khai của người dùng có hành vi tràn chính xác), vì vậy truy cập bộ nhớ nhanh vào loại lớn nhất tôi có thể là trung tâm cho hiệu suất của thuật toán của tôi. Đây là những kết quả của tôi:
Thời gian (tính bằng giây) để hoàn thành chạy thử nghiệm SHA-2
std::copy memcpy % increase
6.11 6.29 2.86%
6.09 6.28 3.03%
6.10 6.29 3.02%
6.08 6.27 3.03%
6.08 6.27 3.03%
Tổng tốc độ tăng trung bình của std :: copy trên memcpy: 2,99%
Trình biên dịch của tôi là gcc 4.6.3 trên Fedora 16 x86_64. Cờ tối ưu hóa của tôi là -Ofast -march=native -funsafe-loop-optimizations
.
Mã cho việc triển khai SHA-2 của tôi.
Tôi cũng quyết định chạy thử nghiệm triển khai MD5. Kết quả kém ổn định hơn nhiều, vì vậy tôi quyết định thực hiện 10 lần chạy. Tuy nhiên, sau vài lần thử đầu tiên, tôi đã nhận được kết quả rất khác nhau từ lần chạy này đến lần tiếp theo, vì vậy tôi đoán có một số hoạt động HĐH đang diễn ra. Tôi quyết định làm lại từ đầu.
Cùng cài đặt trình biên dịch và cờ. Chỉ có một phiên bản MD5 và nó nhanh hơn SHA-2, vì vậy tôi đã thực hiện 3000 vòng trên một bộ 5 chuỗi thử nghiệm tương tự.
Đây là 10 kết quả cuối cùng của tôi:
Thời gian (tính bằng giây) để hoàn thành chạy thử nghiệm MD5
std::copy memcpy % difference
5.52 5.56 +0.72%
5.56 5.55 -0.18%
5.57 5.53 -0.72%
5.57 5.52 -0.91%
5.56 5.57 +0.18%
5.56 5.57 +0.18%
5.56 5.53 -0.54%
5.53 5.57 +0.72%
5.59 5.57 -0.36%
5.57 5.56 -0.18%
Tổng tốc độ giảm trung bình của std :: copy trên memcpy: 0,11%
Mã cho việc thực hiện MD5 của tôi
Những kết quả này cho thấy có một số tối ưu hóa mà std :: copy được sử dụng trong các thử nghiệm SHA-2 std::copy
của tôi không thể sử dụng trong các thử nghiệm MD5 của tôi. Trong các thử nghiệm SHA-2, cả hai mảng được tạo trong cùng một hàm gọi là std::copy
/memcpy
. Trong các thử nghiệm MD5 của tôi, một trong các mảng được truyền vào hàm dưới dạng tham số hàm.
Tôi đã làm thêm một chút thử nghiệm để xem những gì tôi có thể làm để làm cho std::copy
nhanh hơn một lần nữa. Câu trả lời hóa ra rất đơn giản: bật tối ưu hóa thời gian liên kết. Đây là những kết quả của tôi khi LTO được bật (tùy chọn -flto in gcc):
Thời gian (tính bằng giây) để hoàn thành chạy thử nghiệm MD5 với -flto
std::copy memcpy % difference
5.54 5.57 +0.54%
5.50 5.53 +0.54%
5.54 5.58 +0.72%
5.50 5.57 +1.26%
5.54 5.58 +0.72%
5.54 5.57 +0.54%
5.54 5.56 +0.36%
5.54 5.58 +0.72%
5.51 5.58 +1.25%
5.54 5.57 +0.54%
Tổng tốc độ tăng trung bình của std :: copy trên memcpy: 0,72%
Tóm lại, dường như không có hình phạt về hiệu suất khi sử dụng std::copy
. Trong thực tế, dường như có một hiệu suất đạt được.
Giải thích kết quả
Vậy tại sao có thể std::copy
tăng hiệu suất?
Đầu tiên, tôi không hy vọng nó sẽ chậm hơn cho bất kỳ triển khai nào, miễn là tối ưu hóa nội tuyến được bật. Tất cả các trình biên dịch nội tuyến tích cực; nó có thể là tối ưu hóa quan trọng nhất vì nó cho phép rất nhiều tối ưu hóa khác. std::copy
có thể (và tôi nghi ngờ tất cả các triển khai trong thế giới thực làm) phát hiện ra rằng các đối số có thể sao chép một cách tầm thường và bộ nhớ được trình bày tuần tự. Điều này có nghĩa là trong trường hợp xấu nhất, khi memcpy
hợp pháp, std::copy
sẽ không thực hiện tồi tệ hơn. Việc triển khai tầm thường của std::copy
việc trì hoãn đó memcpy
phải đáp ứng tiêu chí của nhà soạn nhạc của bạn là "luôn luôn nội tuyến này khi tối ưu hóa tốc độ hoặc kích thước".
Tuy nhiên, std::copy
cũng giữ nhiều thông tin hơn. Khi bạn gọi std::copy
, chức năng giữ nguyên các loại. memcpy
hoạt động trên void *
, loại bỏ gần như tất cả các thông tin hữu ích. Ví dụ, nếu tôi chuyển vào một mảng std::uint64_t
, trình biên dịch hoặc trình triển khai thư viện có thể có thể tận dụng sự liên kết 64 bit với std::copy
, nhưng có thể khó thực hiện hơn với memcpy
. Nhiều triển khai các thuật toán như công việc này bằng cách đầu tiên làm việc trên phần không được phân bổ ở đầu phạm vi, sau đó là phần được căn chỉnh, sau đó là phần không được phân bổ ở cuối. Nếu tất cả được đảm bảo để được căn chỉnh, thì mã trở nên đơn giản và nhanh hơn và dễ dàng hơn cho bộ dự đoán nhánh trong bộ xử lý của bạn để có được chính xác.
Tối ưu hóa sớm?
std::copy
là ở một vị trí thú vị. Tôi hy vọng nó sẽ không bao giờ chậm hơn memcpy
và đôi khi nhanh hơn với bất kỳ trình biên dịch tối ưu hóa hiện đại nào. Hơn nữa, bất cứ điều gì bạn có thể memcpy
, bạn có thể std::copy
. memcpy
không cho phép bất kỳ sự trùng lặp trong bộ đệm, trong khi std::copy
hỗ trợ chồng lên nhau theo một hướng (với std::copy_backward
cho một hướng khác của chồng lên nhau). memcpy
chỉ hoạt động trên con trỏ, std::copy
hoạt động trên bất kỳ lặp ( std::map
, std::vector
, std::deque
, hoặc riêng kiểu tùy chỉnh của tôi). Nói cách khác, bạn chỉ nên sử dụng std::copy
khi bạn cần sao chép khối dữ liệu xung quanh.
char
có thể được ký hoặc không dấu, tùy thuộc vào việc thực hiện. Nếu số lượng byte có thể> = 128, thì hãy sử dụngunsigned char
cho mảng byte của bạn. (Các(int *)
diễn viên cũng sẽ an toàn hơn(unsigned int *)
.)