Tại sao memmove nhanh hơn memcpy?


89

Tôi đang điều tra các điểm nóng về hiệu suất trong một ứng dụng dành 50% thời gian của nó trong memmove (3). Ứng dụng sẽ chèn hàng triệu số nguyên 4 byte vào các mảng đã sắp xếp và sử dụng memmove để chuyển dữ liệu "sang phải" nhằm tạo khoảng trống cho giá trị được chèn.

Kỳ vọng của tôi là việc sao chép bộ nhớ cực kỳ nhanh, và tôi rất ngạc nhiên khi dành quá nhiều thời gian cho bộ nhớ. Nhưng sau đó tôi có ý tưởng rằng memmove chậm vì nó di chuyển các vùng chồng chéo, phải được thực hiện theo một vòng lặp chặt chẽ, thay vì sao chép các trang bộ nhớ lớn. Tôi đã viết một microbenchmark nhỏ để tìm hiểu xem liệu có sự khác biệt về hiệu suất giữa memcpy và memmove hay không, mong rằng memcpy sẽ thắng.

Tôi đã chạy điểm chuẩn của mình trên hai máy (core i5, core i7) và thấy rằng memmove thực sự nhanh hơn memcpy, trên core i7 cũ hơn thậm chí còn nhanh hơn gần gấp đôi! Bây giờ tôi đang tìm kiếm lời giải thích.

Đây là điểm chuẩn của tôi. Nó sao chép 100 mb với memcpy, và sau đó di chuyển khoảng 100 mb với memmove; nguồn và đích chồng chéo. Nhiều "khoảng cách" khác nhau cho nguồn và đích được thử. Mỗi bài test chạy 10 lần, thời gian in trung bình.

https://gist.github.com/cruppstahl/78a57cdf937bca3d062c

Đây là kết quả trên Core i5 (Linux 3.5.0-54-generic # 81 ~ precision1-Ubuntu SMP x86_64 GNU / Linux, gcc là 4.6.3 (Ubuntu / Linaro 4.6.3-1ubuntu5). Con số trong ngoặc là khoảng cách (kích thước khoảng cách) giữa nguồn và đích:

memcpy        0.0140074
memmove (002) 0.0106168
memmove (004) 0.01065
memmove (008) 0.0107917
memmove (016) 0.0107319
memmove (032) 0.0106724
memmove (064) 0.0106821
memmove (128) 0.0110633

Memmove được thực hiện dưới dạng mã trình hợp dịch được tối ưu hóa SSE, sao chép từ sau ra trước. Nó sử dụng tính năng tìm nạp trước phần cứng để tải dữ liệu vào bộ đệm và sao chép 128 byte vào các thanh ghi XMM, sau đó lưu trữ chúng tại đích.

( memcpy-ssse3-back.S , dòng 1650 ff)

L(gobble_ll_loop):
    prefetchnta -0x1c0(%rsi)
    prefetchnta -0x280(%rsi)
    prefetchnta -0x1c0(%rdi)
    prefetchnta -0x280(%rdi)
    sub $0x80, %rdx
    movdqu  -0x10(%rsi), %xmm1
    movdqu  -0x20(%rsi), %xmm2
    movdqu  -0x30(%rsi), %xmm3
    movdqu  -0x40(%rsi), %xmm4
    movdqu  -0x50(%rsi), %xmm5
    movdqu  -0x60(%rsi), %xmm6
    movdqu  -0x70(%rsi), %xmm7
    movdqu  -0x80(%rsi), %xmm8
    movdqa  %xmm1, -0x10(%rdi)
    movdqa  %xmm2, -0x20(%rdi)
    movdqa  %xmm3, -0x30(%rdi)
    movdqa  %xmm4, -0x40(%rdi)
    movdqa  %xmm5, -0x50(%rdi)
    movdqa  %xmm6, -0x60(%rdi)
    movdqa  %xmm7, -0x70(%rdi)
    movdqa  %xmm8, -0x80(%rdi)
    lea -0x80(%rsi), %rsi
    lea -0x80(%rdi), %rdi
    jae L(gobble_ll_loop)

Tại sao memmove nhanh hơn memcpy? Tôi mong đợi memcpy sao chép các trang bộ nhớ, sẽ nhanh hơn nhiều so với việc lặp lại. Trong trường hợp xấu nhất, tôi sẽ mong đợi memcpy nhanh như memmove.

Tái bút: Tôi biết rằng tôi không thể thay thế memmove bằng memcpy trong mã của mình. Tôi biết rằng mẫu mã kết hợp C và C ++. Câu hỏi này thực sự chỉ dành cho mục đích học thuật.

CẬP NHẬT 1

Tôi đã chạy một số biến thể của bài kiểm tra, dựa trên các câu trả lời khác nhau.

  1. Khi chạy memcpy hai lần, thì lần chạy thứ hai nhanh hơn lần chạy thứ nhất.
  2. Khi "chạm" vào vùng đệm đích của memcpy (memset(b2, 0, BUFFERSIZE...) ) thì lần chạy đầu tiên của memcpy cũng nhanh hơn.
  3. memcpy vẫn chậm hơn memmove một chút.

Đây là kết quả:

memcpy        0.0118526
memcpy        0.0119105
memmove (002) 0.0108151
memmove (004) 0.0107122
memmove (008) 0.0107262
memmove (016) 0.0108555
memmove (032) 0.0107171
memmove (064) 0.0106437
memmove (128) 0.0106648

Kết luận của tôi: dựa trên nhận xét từ @Oliver Charlesworth, hệ điều hành phải cam kết bộ nhớ vật lý ngay sau khi bộ đệm đích memcpy được truy cập lần đầu tiên (nếu ai đó biết cách "chứng minh" điều này thì hãy thêm câu trả lời! ). Ngoài ra, như @Mats Petersson đã nói, memmove thân thiện với bộ nhớ cache hơn memcpy.

Cảm ơn vì tất cả những câu trả lời và nhận xét tuyệt vời!


1
Bạn đã nhìn vào mã memmove, bạn cũng nhìn vào mã memcpy?
Oliver Charlesworth

8
Kỳ vọng của tôi là sao chép bộ nhớ cực kỳ nhanh - chỉ khi bộ nhớ nằm trong bộ nhớ đệm L1. Khi dữ liệu không vừa trong bộ nhớ cache, hiệu suất sao chép của bạn sẽ giảm đi.
Maxim Egorushkin

1
BTW, bạn chỉ sao chép một nhánh của memmove. Nhánh này không thể xử lý việc di chuyển khi nguồn chồng lên đích và đích ở các địa chỉ thấp hơn.
Maxim Egorushkin

2
Tôi chưa có thời gian tiếp cận máy Linux nên chưa thể kiểm tra lý thuyết này. Nhưng một lời giải thích có thể khác là áp dụng quá nhiều ; memcpyvòng lặp của bạn là lần đầu tiên nội dung của b2được truy cập, do đó hệ điều hành phải cấp bộ nhớ vật lý cho nó khi nó hoạt động.
Oliver Charlesworth

2
Tái bút: Nếu đây là một nút thắt cổ chai, tôi sẽ xem xét lại cách tiếp cận. Làm thế nào về việc đặt các giá trị vào một danh sách hoặc cấu trúc cây (ví dụ cây nhị phân) và sau đó đọc chúng thành một mảng ở cuối. Các nút theo cách tiếp cận như vậy sẽ là một ứng cử viên tuyệt vời cho việc phân bổ nhóm. Chúng chỉ được thêm vào cho đến cuối khi chúng được phát hành hàng loạt. Điều đó đặc biệt đúng nếu bạn biết mình sẽ cần bao nhiêu khi bắt đầu. Các thư viện tăng cường có một bộ phân bổ nhóm.
Persixty

Câu trả lời:


56

Các memmovecuộc gọi của bạn đang xáo trộn bộ nhớ từ 2 đến 128 byte, trong khi memcpynguồn và đích của bạn hoàn toàn khác nhau. Bằng cách nào đó, điều đó giải thích cho sự khác biệt về hiệu suất: nếu bạn sao chép vào cùng một nơi, bạn sẽ thấy memcpykết quả có thể nhanh hơn, ví dụ: trên Ideone.com :

memmove (002) 0.0610362
memmove (004) 0.0554264
memmove (008) 0.0575859
memmove (016) 0.057326
memmove (032) 0.0583542
memmove (064) 0.0561934
memmove (128) 0.0549391
memcpy 0.0537919

Mặc dù vậy, hầu như không có gì trong đó - không có bằng chứng nào cho thấy việc viết lại trang đã bị lỗi trong bộ nhớ có nhiều tác động và chúng tôi chắc chắn không thấy giảm một nửa thời gian ... nhưng nó cho thấy rằng không có gì sai khi làm memcpychậm hơn không cần thiết khi so sánh táo -cho-táo.


Tôi đã mong đợi rằng bộ đệm của CPU không gây ra sự khác biệt vì bộ đệm của tôi lớn hơn nhiều so với bộ đệm.
cruppstahl

2
Nhưng mỗi thứ yêu cầu tổng số lần truy cập bộ nhớ chính như nhau, phải không? (Tức là 100 MB đọc và 100 MB ghi). Mẫu bộ nhớ cache không làm tròn điều đó. Vì vậy, cách duy nhất để cái này có thể chậm hơn cái kia là nếu một số nội dung phải được đọc / ghi từ / vào bộ nhớ nhiều hơn một lần.
Oliver Charlesworth

2
@Tony D - Kết luận của tôi là hỏi những người thông minh hơn tôi;)
cruppstahl

1
Ngoài ra, điều gì sẽ xảy ra nếu bạn sao chép vào cùng một nơi, nhưng thực hiện memcpylại lần đầu tiên?
Oliver Charlesworth

1
@OliverCharlesworth: lần chạy thử nghiệm đầu tiên luôn có kết quả đáng kể, nhưng thực hiện hai lần kiểm tra memcpy: memcpy 0,0688002 0,0583162 | memmove 0,0577443 0,05862 0,0601029 ... xem ideone.com/8EEAcA
Tony Delroy

24

Khi bạn đang sử dụng memcpy, việc ghi cần phải đi vào bộ nhớ đệm. Khi bạn sử dụng memmoveở đâu khi bạn đang sao chép một bước nhỏ về phía trước, bộ nhớ bạn đang sao chép sẽ nằm trong bộ nhớ cache (vì nó đã được đọc 2, 4, 16 hoặc 128 byte "trở lại"). Hãy thử làm mộtmemmove trong đó đích đến là vài megabyte (kích thước bộ nhớ cache> 4 *) và tôi nghi ngờ (nhưng không phiền khi kiểm tra) rằng bạn sẽ nhận được kết quả tương tự.

Tôi đảm bảo rằng TẤT CẢ là về bảo trì bộ nhớ cache khi bạn thực hiện các hoạt động với bộ nhớ lớn.


+1 Tôi nghĩ vì những lý do bạn đã đề cập, một memmove lặp ngược là bộ nhớ cache thân thiện hơn memcpy. Tuy nhiên, tôi phát hiện ra rằng khi chạy thử nghiệm memcpy hai lần, lần chạy thứ hai nhanh như memmove. Tại sao? Bộ đệm quá lớn nên lần chạy thứ hai của memcpy sẽ không hiệu quả (theo bộ nhớ cache) như lần chạy đầu tiên. Vì vậy, có vẻ như có các yếu tố bổ sung ở đây gây ra hình phạt hiệu suất.
cruppstahl

3
Với những trường hợp phù hợp, một giây memcpysẽ nhanh hơn đáng kể chỉ vì TLB đã được điền sẵn. Ngoài ra, một giây memcpysẽ không phải làm trống bộ nhớ cache của những thứ bạn có thể cần "loại bỏ" (các dòng bộ nhớ cache bẩn "có hại" cho hiệu suất theo nhiều cách. Tuy nhiên, để nói chắc chắn, bạn cần phải chạy một cái gì đó như "perf" và lấy mẫu những thứ như lỗi bộ nhớ cache, lỗi TLB, v.v.
Mats Petersson

15

Về mặt lịch sử, ghi nhớ và ghi nhớ có cùng chức năng. Họ đã làm việc theo cùng một cách và thực hiện giống nhau. Sau đó, người ta nhận ra rằng bản ghi nhớ không cần (và thường là không) được định nghĩa để xử lý các vùng chồng chéo theo bất kỳ cách cụ thể nào.

Kết quả cuối cùng là memmove được xác định để xử lý các vùng chồng chéo theo một cách cụ thể ngay cả khi điều này ảnh hưởng đến hiệu suất. Memcopy được cho là sử dụng thuật toán tốt nhất hiện có cho các vùng không chồng chéo. Việc triển khai thường gần như giống hệt nhau.

Vấn đề mà bạn gặp phải là có quá nhiều biến thể của phần cứng x86 đến mức không thể biết được phương pháp chuyển bộ nhớ nào sẽ nhanh nhất. Và ngay cả khi bạn nghĩ rằng bạn gặp phải một kết quả trong một trường hợp nào đó đơn giản như việc có một 'bước tiến' khác trong bố cục bộ nhớ có thể gây ra hiệu suất bộ nhớ cache khác nhau rất nhiều.

Bạn có thể đánh giá điểm chuẩn những gì bạn đang thực sự làm hoặc bỏ qua vấn đề và dựa vào các điểm chuẩn được thực hiện cho thư viện C.

Chỉnh sửa: Ồ, và một điều cuối cùng; chuyển nhiều nội dung bộ nhớ xung quanh RẤT chậm. Tôi đoán ứng dụng của bạn sẽ chạy nhanh hơn với một cái gì đó giống như một triển khai B-Tree đơn giản để xử lý các số nguyên của bạn. (Ồ, không sao đâu)

Edit2: Để tóm tắt phần mở rộng của tôi trong phần nhận xét: Microbenchmark là vấn đề ở đây, nó không đo lường những gì bạn nghĩ. Các nhiệm vụ được trao cho memcpy và memmove khác nhau đáng kể. Nếu nhiệm vụ được giao cho memcpy được lặp lại nhiều lần với memmove hoặc memcpy, kết quả cuối cùng sẽ không phụ thuộc vào chức năng chuyển bộ nhớ nào bạn sử dụng BẤT CHẤP các vùng chồng lên nhau.


Nhưng đó là những gì về - tôi đang đo điểm chuẩn những gì tôi thực sự đang làm. Câu hỏi này là về việc giải thích kết quả của điểm chuẩn, mâu thuẫn với những gì bạn đang tuyên bố - rằng bản ghi nhớ nhanh hơn đối với các vùng không chồng chéo.
cruppstahl

Ứng dụng của tôi một cây b! Bất cứ khi nào các số nguyên được chèn vào một memmove nút lá được gọi để tạo khoảng trống. Tôi đang làm việc trên một công cụ cơ sở dữ liệu.
cruppstahl

1
Bạn đang sử dụng một điểm chuẩn vi mô và bạn thậm chí không có bản ghi nhớ và bản ghi nhớ thay đổi cùng một dữ liệu. Các vị trí chính xác trong bộ nhớ mà dữ liệu bạn đang xử lý sẽ tạo ra sự khác biệt đối với bộ nhớ đệm và số lượng vòng quay tới bộ nhớ mà CPU phải thực hiện.
user3710044

Mặc dù câu trả lời này đúng, nhưng nó không thực sự giải thích tại sao nó chậm hơn trong trường hợp này, về cơ bản nó nói rằng "nó chậm hơn vì trong một số trường hợp, nó có thể chậm hơn".
Oliver Charlesworth

Tôi đang nói rằng đối với các trường hợp giống nhau, bao gồm bố cục bộ nhớ giống nhau để sao chép / di chuyển các điểm chuẩn SẼ giống nhau bởi vì việc triển khai giống nhau. Vấn đề là ở microbenchmark.
user3710044

2

"memcpy hiệu quả hơn memmove." Trong trường hợp của bạn, rất có thể bạn đang không thực hiện cùng một việc trong khi chạy hai hàm.

Nói chung, chỉ SỬ DỤNG memmove nếu bạn phải. SỬ DỤNG nó khi có khả năng rất hợp lý rằng vùng nguồn và vùng đích đang vượt quá giới hạn.

Tham khảo: https://www.youtube.com/watch?v=Yr1YnOVG-4g Tiến sĩ Jerry Cain, (Bài giảng Hệ thống nội bộ của Stanford - 7) Thời gian: 36:00

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.