Có rất nhiều câu trả lời hay ở đây đã bao gồm nhiều điểm nổi bật, vì vậy tôi sẽ chỉ thêm một vài vấn đề mà tôi không thấy được giải quyết trực tiếp ở trên. Đó là, câu trả lời này không nên được coi là toàn diện về ưu và nhược điểm, mà là phần phụ lục cho các câu trả lời khác ở đây.
mmap có vẻ như ma thuật
Lấy trường hợp tập tin đã được lưu trữ đầy đủ 1 làm cơ sở 2 , mmap
có thể trông khá giống ma thuật :
mmap
chỉ yêu cầu 1 cuộc gọi hệ thống để (có khả năng) ánh xạ toàn bộ tệp, sau đó không cần thêm các cuộc gọi hệ thống nữa.
mmap
không yêu cầu bản sao dữ liệu tệp từ kernel sang không gian người dùng.
mmap
cho phép bạn truy cập tệp "dưới dạng bộ nhớ", bao gồm xử lý tệp bằng bất kỳ thủ thuật nâng cao nào bạn có thể làm đối với bộ nhớ, chẳng hạn như tự động vector hóa trình biên dịch, nội tại SIMD , tìm nạp trước, các thói quen phân tích cú pháp trong bộ nhớ được tối ưu hóa, OpenMP, v.v.
Trong trường hợp tệp đã có trong bộ đệm, dường như không thể đánh bại: bạn chỉ cần truy cập trực tiếp vào bộ đệm của trang kernel làm bộ nhớ và nó không thể nhanh hơn thế.
Vâng, nó có thể.
mmap không thực sự kỳ diệu bởi vì ...
mmap vẫn hoạt động trên mỗi trang
Một chi phí ẩn chính mmap
so với read(2)
(thực sự là tòa nhà chọc trời ở cấp độ hệ điều hành tương đương để đọc các khối ) là với việc mmap
bạn sẽ cần thực hiện "một số công việc" cho mọi trang 4K trong không gian người dùng, mặc dù có thể bị ẩn bởi cơ chế lỗi trang.
Ví dụ, một triển khai điển hình chỉ mmap
toàn bộ tệp sẽ cần phải sửa lỗi 100 GB / 4K = 25 triệu lỗi để đọc tệp 100 GB. Bây giờ, đây sẽ là những lỗi nhỏ , nhưng lỗi 25 tỷ trang vẫn sẽ không được siêu nhanh. Chi phí của một lỗi nhỏ có lẽ là trong 100 nano trong trường hợp tốt nhất.
mmap phụ thuộc rất nhiều vào hiệu suất TLB
Bây giờ, bạn có thể chuyển qua MAP_POPULATE
để mmap
bảo nó thiết lập tất cả các bảng trang trước khi quay lại, do đó sẽ không có lỗi trang trong khi truy cập nó. Bây giờ, có một vấn đề nhỏ là nó cũng đọc toàn bộ tệp vào RAM, điều này sẽ nổ tung nếu bạn cố gắng ánh xạ tệp 100 GB - nhưng bây giờ hãy bỏ qua điều đó 3 . Kernel cần thực hiện công việc trên mỗi trang để thiết lập các bảng trang này (hiển thị dưới dạng thời gian kernel). Điều này kết thúc là một chi phí lớn trong mmap
cách tiếp cận và nó tỷ lệ thuận với kích thước tệp (nghĩa là, nó không trở nên tương đối ít quan trọng hơn khi kích thước tệp tăng lên) 4 .
Cuối cùng, ngay cả trong không gian người dùng truy cập một ánh xạ như vậy không hoàn toàn miễn phí (so với bộ đệm bộ nhớ lớn không có nguồn gốc từ tệp mmap
) - ngay cả khi các bảng trang được thiết lập, mỗi lần truy cập vào một trang mới sẽ về mặt khái niệm, phát sinh một TLB bỏ lỡ. Từmmap
một tệp có nghĩa là sử dụng bộ đệm trang và các trang 4K của nó, bạn lại phải chịu chi phí này 25 triệu lần cho một tệp 100 GB.
Bây giờ, chi phí thực tế của các lỗi TLB này phụ thuộc rất nhiều vào ít nhất các khía cạnh sau của phần cứng của bạn: (a) bạn có bao nhiêu 4K TLB và phần còn lại của bộ nhớ đệm dịch hoạt động như thế nào (b) khả năng tìm nạp trước phần cứng tốt như thế nào với TLB - ví dụ: có thể tìm nạp trước một trang đi bộ không? (c) phần cứng của trang đi bộ nhanh như thế nào và nhanh như thế nào. Trên các bộ xử lý Intel x86 cao cấp hiện đại, phần cứng đi bộ nói chung rất mạnh: có ít nhất 2 máy đi bộ trang song song, việc đi bộ trang có thể xảy ra đồng thời với việc tiếp tục thực hiện và việc tìm nạp trước phần cứng có thể kích hoạt việc đi bộ trang. Vì vậy, TLB tác động lên một luồng đọc khá thấp - và tải như vậy thường sẽ thực hiện tương tự bất kể kích thước trang. Phần cứng khác thường là tồi tệ hơn nhiều, tuy nhiên!
đọc () tránh những cạm bẫy
Tòa nhà read()
, đó là những gì thường làm cơ sở cho các cuộc gọi loại "đọc khối" được cung cấp, ví dụ, trong C, C ++ và các ngôn ngữ khác có một nhược điểm chính mà mọi người đều biết rõ:
- Mỗi
read()
cuộc gọi của N byte phải sao chép N byte từ kernel vào không gian người dùng.
Mặt khác, nó tránh được hầu hết các chi phí ở trên - bạn không cần ánh xạ 25 triệu trang 4K vào không gian người dùng. Bạn thường có thể malloc
một bộ đệm nhỏ bộ đệm trong không gian người dùng và sử dụng lại nhiều lần cho tất cả các read
cuộc gọi của bạn . Về phía kernel, hầu như không có vấn đề gì với các trang 4K hoặc TLB bỏ lỡ vì tất cả RAM thường được ánh xạ tuyến tính bằng cách sử dụng một vài trang rất lớn (ví dụ: các trang 1 GB trên x86), do đó các trang bên dưới trong bộ đệm của trang được che kín rất hiệu quả trong không gian kernel.
Vì vậy, về cơ bản, bạn có so sánh sau để xác định cái nào nhanh hơn cho một lần đọc một tệp lớn:
Là công việc trên mỗi trang bổ sung được ngụ ý bởi mmap
cách tiếp cận tốn kém hơn công việc trên mỗi byte sao chép nội dung tệp từ kernel sang không gian người dùng ngụ ý bằng cách sử dụng read()
?
Trên nhiều hệ thống, chúng thực sự cân bằng. Lưu ý rằng mỗi một tỷ lệ với các thuộc tính hoàn toàn khác nhau của ngăn xếp phần cứng và hệ điều hành.
Cụ thể, mmap
cách tiếp cận trở nên tương đối nhanh hơn khi:
- HĐH có khả năng xử lý lỗi nhỏ nhanh và đặc biệt là tối ưu hóa lỗi nhỏ như lỗi xung quanh.
- HĐH có tốt
MAP_POPULATE
triển khai có thể xử lý hiệu quả các bản đồ lớn trong trường hợp, ví dụ, các trang bên dưới nằm liền kề trong bộ nhớ vật lý.
- Phần cứng có hiệu suất dịch trang mạnh, chẳng hạn như TLB lớn, TLB cấp hai nhanh, trình duyệt trang nhanh và song song, tương tác tìm nạp trước tốt với dịch thuật, v.v.
... Trong khi read()
cách tiếp cận trở nên tương đối nhanh hơn khi:
- Tòa nhà
read()
có hiệu suất sao chép tốt. Ví dụ, copy_to_user
hiệu suất tốt về phía hạt nhân.
- Nhân có một cách hiệu quả (liên quan đến vùng người dùng) để ánh xạ bộ nhớ, ví dụ, chỉ sử dụng một vài trang lớn có hỗ trợ phần cứng.
- Hạt nhân có các tòa nhà chọc trời nhanh và một cách để giữ các mục nhập TLB của hạt nhân xung quanh các tòa nhà.
Các yếu tố phần cứng trên khác nhau một cách hoang dại khác nhau giữa các nền tảng khác nhau, ngay cả trong cùng một gia đình (ví dụ: trong các thế hệ x86 và đặc biệt là các phân khúc thị trường) và chắc chắn trên các kiến trúc (ví dụ: ARM vs x86 so với PPC).
Các yếu tố HĐH cũng liên tục thay đổi, với nhiều cải tiến ở cả hai phía gây ra bước nhảy lớn về tốc độ tương đối cho cách tiếp cận này hay cách khác. Một danh sách gần đây bao gồm:
- Bổ sung các lỗi xung quanh, được mô tả ở trên, thực sự giúp ích cho
mmap
trường hợp không có MAP_POPULATE
.
- Bổ sung các
copy_to_user
phương thức đường dẫn nhanh arch/x86/lib/copy_user_64.S
, ví dụ, sử dụng REP MOVQ
khi nó nhanh, thực sự giúp ích cho read()
trường hợp.
Cập nhật sau Spectre và Meltdown
Các giảm thiểu cho các lỗ hổng Spectre và Meltdown đã làm tăng đáng kể chi phí của một cuộc gọi hệ thống. Trên các hệ thống tôi đã đo, chi phí của một cuộc gọi hệ thống "không làm gì" (đó là ước tính chi phí hoạt động của cuộc gọi hệ thống, ngoài bất kỳ công việc thực tế nào được thực hiện bởi cuộc gọi) đã đi từ khoảng 100 ns trên một điển hình hệ thống Linux hiện đại đến khoảng 700 ns. Hơn nữa, tùy thuộc vào hệ thống của bạn, bản sửa lỗi cách ly bảng trang dành riêng cho Meltdown có thể có thêm hiệu ứng xuôi dòng ngoài chi phí cuộc gọi hệ thống trực tiếp do nhu cầu tải lại các mục TLB.
Tất cả điều này là một bất lợi tương đối cho read()
các phương thức dựa trên so với mmap
các phương thức dựa trên, vì read()
các phương thức phải thực hiện một cuộc gọi hệ thống cho mỗi giá trị "kích thước bộ đệm" của dữ liệu. Bạn không thể tùy ý tăng kích thước bộ đệm để khấu hao chi phí này vì việc sử dụng bộ đệm lớn thường hoạt động kém hơn do bạn vượt quá kích thước L1 và do đó liên tục bị lỗi bộ nhớ cache.
Mặt khác, với mmap
, bạn có thể ánh xạ trong một vùng bộ nhớ lớn MAP_POPULATE
và truy cập nó một cách hiệu quả, với chi phí chỉ bằng một cuộc gọi hệ thống duy nhất.
1 Điều này ít nhiều cũng bao gồm cả trường hợp tập tin không được lưu bộ nhớ cache đầy đủ để bắt đầu, nhưng trong đó hệ điều hành đọc trước đủ tốt để làm cho nó xuất hiện (vì vậy, trang thường được lưu vào bộ nhớ cache khi bạn muốn nó). Đây là một vấn đề tế nhị mặc dù bởi vì đường read-ahead công trình thường là khá khác nhau giữa mmap
và read
các cuộc gọi, và có thể được điều chỉnh hơn nữa bởi các cuộc gọi "tư vấn" như mô tả trong 2 .
2 ... bởi vì nếu tệp không được lưu trong bộ nhớ cache, hành vi của bạn sẽ bị chi phối hoàn toàn bởi các mối quan tâm của IO, bao gồm cả kiểu truy cập của bạn đối với phần cứng cơ bản như thế nào - và tất cả nỗ lực của bạn phải đảm bảo quyền truy cập đó cũng thông cảm như có thể, ví dụ như thông qua việc sử dụng madvise
hoặc fadvise
gọi (và bất kỳ thay đổi cấp độ ứng dụng nào bạn có thể thực hiện để cải thiện các mẫu truy cập).
3 Bạn có thể khắc phục điều đó, ví dụ, bằng cách tuần tự mmap
vào các cửa sổ có kích thước nhỏ hơn, giả sử 100 MB.
4 Trong thực tế, nó quay ra các MAP_POPULATE
cách tiếp cận là (ít nhất một số phần cứng / OS kết hợp) chỉ hơi nhanh hơn so với không sử dụng nó, có lẽ vì hạt nhân đang sử dụng faultaround - vì vậy con số thực tế của các đứt gãy nhỏ bị giảm bởi một yếu tố của 16 hoặc là.
mmap()
nhanh hơn 2-6 lần so với sử dụng các tòa nhà chọc trời , vdread()
.