mmap () so với khối đọc


184

Tôi đang làm việc trên một chương trình sẽ xử lý các tệp có khả năng có kích thước 100GB trở lên. Các tập tin chứa các bộ hồ sơ chiều dài thay đổi. Tôi đã có một triển khai đầu tiên và chạy và hiện đang tìm cách cải thiện hiệu suất, đặc biệt là thực hiện I / O hiệu quả hơn vì tệp đầu vào được quét nhiều lần.

Có quy tắc nào để sử dụng mmap()so với đọc trong các khối thông qua fstreamthư viện của C ++ không? Những gì tôi muốn làm là đọc các khối lớn từ đĩa vào bộ đệm, xử lý các bản ghi hoàn chỉnh từ bộ đệm và sau đó đọc thêm.

Các mmap()mã có thể có khả năng nhận rất lộn xộn từ mmap'd khối cần phải nằm trên trang có kích thước ranh giới (hiểu biết của tôi) và các hồ sơ có thể có khả năng như qua các biên giới trang. Vớifstream s, tôi chỉ có thể tìm cách bắt đầu một bản ghi và bắt đầu đọc lại, vì chúng tôi không giới hạn ở các khối đọc nằm trên ranh giới kích thước trang.

Làm thế nào tôi có thể quyết định giữa hai tùy chọn này mà không thực sự viết ra một triển khai hoàn chỉnh trước? Bất kỳ quy tắc nào (ví dụ, mmap()nhanh hơn 2 lần) hoặc các bài kiểm tra đơn giản?


1
Đây là một bài đọc thú vị: Medium.com/@sasha_f/ Khăn Trong các thí nghiệm mmap()nhanh hơn 2-6 lần so với sử dụng các tòa nhà chọc trời , vd read().
mplattner

Câu trả lời:


208

Tôi đã cố gắng tìm từ cuối cùng về hiệu suất mmap / read trên Linux và tôi đã tìm thấy một bài đăng ( liên kết ) đẹp trong danh sách gửi thư của nhân Linux. Đó là từ năm 2000, do đó đã có nhiều cải tiến đối với IO và bộ nhớ ảo trong kernel kể từ đó, nhưng nó giải thích độc đáo lý do tại sao mmaphoặcread có thể nhanh hơn hoặc chậm hơn.

  • Một cuộc gọi đến mmapcó nhiều chi phí hơn read(giống như epollcó nhiều chi phí hơnpoll , có nhiều chi phí hơnread ). Thay đổi ánh xạ bộ nhớ ảo là một hoạt động khá tốn kém trên một số bộ xử lý vì cùng lý do rằng việc chuyển đổi giữa các quy trình khác nhau rất tốn kém.
  • Hệ thống IO đã có thể sử dụng bộ đệm đĩa, vì vậy nếu bạn đọc một tệp, bạn sẽ nhấn bộ đệm hoặc bỏ lỡ nó cho dù bạn sử dụng phương pháp nào.

Tuy nhiên,

  • Bản đồ bộ nhớ thường nhanh hơn để truy cập ngẫu nhiên, đặc biệt nếu các mẫu truy cập của bạn thưa thớt và không thể đoán trước.
  • Bản đồ bộ nhớ cho phép bạn tiếp tục sử dụng các trang từ bộ đệm cho đến khi bạn hoàn thành. Điều này có nghĩa là nếu bạn sử dụng một tệp nhiều trong một thời gian dài, sau đó đóng nó và mở lại, các trang sẽ vẫn được lưu trữ. Với read, tập tin của bạn có thể đã bị xóa từ bộ đệm từ lâu. Điều này không áp dụng nếu bạn sử dụng một tập tin và ngay lập tức loại bỏ nó. (Nếu bạn cố gắng để mlockcác trang chỉ để giữ chúng trong bộ đệm, bạn đang cố gắng vượt quá bộ đệm đĩa và loại lừa này hiếm khi giúp hiệu năng hệ thống).
  • Đọc một tập tin trực tiếp rất đơn giản và nhanh chóng.

Cuộc thảo luận về mmap / read làm tôi nhớ đến hai cuộc thảo luận về hiệu suất khác:

  • Một số lập trình viên Java đã bị sốc khi phát hiện ra rằng I / O không chặn thường chậm hơn so với chặn I / O, điều này có ý nghĩa hoàn hảo nếu bạn biết rằng I / O không chặn yêu cầu tạo ra nhiều tòa nhà hơn.

  • Một số lập trình viên mạng khác đã bị sốc khi biết rằng epollthường chậm hơn poll, điều này hoàn toàn hợp lý nếu bạn biết rằng việc quản lý epollđòi hỏi phải tạo ra nhiều tòa nhà hơn.

Kết luận: Sử dụng bản đồ bộ nhớ nếu bạn truy cập dữ liệu ngẫu nhiên, giữ nó trong một thời gian dài hoặc nếu bạn biết bạn có thể chia sẻ nó với các quy trình khác ( MAP_SHAREDkhông thú vị nếu không có chia sẻ thực tế). Đọc tệp bình thường nếu bạn truy cập dữ liệu tuần tự hoặc loại bỏ nó sau khi đọc. Và nếu một trong hai phương pháp làm cho chương trình của bạn ít phức tạp hơn, hãy làm điều đó . Đối với nhiều trường hợp trong thế giới thực, không có cách nào chắc chắn để hiển thị một cách nhanh hơn mà không cần kiểm tra ứng dụng thực tế của bạn và KHÔNG phải là điểm chuẩn.

(Xin lỗi vì đã không biết câu hỏi này, nhưng tôi đang tìm câu trả lời và câu hỏi này tiếp tục xuất hiện ở đầu kết quả của Google.)


Hãy nhớ rằng sử dụng bất kỳ lời khuyên nào dựa trên phần cứng và phần mềm từ những năm 2000, mà không cần kiểm tra ngày hôm nay sẽ là một cách tiếp cận rất đáng ngờ. Ngoài ra, trong khi nhiều sự thật về mmapvs read()trong chủ đề đó vẫn đúng như trong quá khứ, hiệu suất tổng thể thực sự không thể được xác định bằng cách thêm các ưu và nhược điểm, mà chỉ bằng cách thử nghiệm trên một cấu hình phần cứng cụ thể. Ví dụ, có thể tranh cãi rằng "Cuộc gọi đến mmap có nhiều chi phí hơn đọc" - có mmapphải thêm ánh xạ vào bảng trang quy trình, nhưng readphải sao chép tất cả các byte đọc từ kernel vào không gian người dùng.
BeeOnRope

Kết quả cuối cùng là, trên phần cứng (Intel hiện đại, vào khoảng năm 2018) của tôi, mmapcó chi phí hoạt động thấp hơn so readvới số lần đọc lớn hơn (4 KiB). Bây giờ rất đúng là nếu bạn muốn truy cập dữ liệu một cách thưa thớt và ngẫu nhiên, mmapthực sự, thực sự tốt - nhưng điều ngược lại là không cần thiết đúng: mmapvẫn có thể là cách tốt nhất để truy cập tuần tự.
BeeOnRope

1
@BeeOnRope: Bạn có thể hoài nghi về lời khuyên dựa trên phần cứng và phần mềm từ những năm 2000, nhưng tôi thậm chí còn nghi ngờ hơn về các điểm chuẩn không cung cấp phương pháp và dữ liệu. Nếu bạn muốn tạo ra một trường hợp mmapnhanh hơn, tôi sẽ mong đợi được nhìn thấy ở mức tối thiểu toàn bộ thiết bị thử nghiệm (mã nguồn) với kết quả được lập bảng và số kiểu máy xử lý.
Dietrich Epp

@BeeOnRope: Ngoài ra, hãy nhớ rằng khi bạn đang kiểm tra các bit của hệ thống bộ nhớ như thế này, các dấu hiệu vi mô có thể cực kỳ lừa đảo vì việc xả TLB có thể ảnh hưởng tiêu cực đến hiệu suất của phần còn lại của chương trình và tác động này sẽ không xuất hiện nếu bạn chỉ đo mmap chính nó.
Dietrich Epp

2
@DietrichEpp - vâng, tôi sẽ thành thạo các hiệu ứng TLB. Lưu ý rằng mmapkhông tuôn ra TLB trừ trường hợp bất thường (nhưng munmapcó thể). Các thử nghiệm của tôi bao gồm cả microbenchmark (bao gồm munmap) cả "trong ứng dụng" đang chạy trong trường hợp sử dụng trong thế giới thực. Tất nhiên ứng dụng của tôi không giống với ứng dụng của bạn, vì vậy mọi người nên kiểm tra cục bộ. Thậm chí còn không rõ ràng mmapđược ưa chuộng bởi một điểm chuẩn vi mô: read()cũng được tăng sức mạnh vì bộ đệm đích phía người dùng thường ở trong L1, điều này có thể không xảy ra trong một ứng dụng lớn hơn. Vì vậy, yeah, "nó phức tạp".
BeeOnRope

47

Chi phí hiệu năng chính sẽ là đĩa i / o. "Mmap ()" chắc chắn nhanh hơn iux, nhưng sự khác biệt có thể không đáng chú ý vì đĩa i / o sẽ chi phối thời gian chạy của bạn.

Tôi đã thử đoạn mã của Ben Collins (xem bên trên / bên dưới) để kiểm tra khẳng định của anh ấy rằng "mmap () là cách nhanh hơn" và không tìm thấy sự khác biệt có thể đo lường được. Xem ý kiến ​​của tôi về câu trả lời của anh ấy.

Tôi chắc chắn sẽ không đề xuất riêng từng lần lượt từng bản ghi trừ khi "bản ghi" của bạn rất lớn - sẽ chậm khủng khiếp, yêu cầu 2 cuộc gọi hệ thống cho mỗi bản ghi và có thể mất trang khỏi bộ nhớ cache của bộ nhớ đĩa .... .

Trong trường hợp của bạn, tôi nghĩ rằng các lệnh gọi mmap (), iux và các lệnh gọi open () / read () ở mức độ thấp sẽ giống nhau. Tôi muốn giới thiệu mmap () trong những trường hợp sau:

  1. Có quyền truy cập ngẫu nhiên (không tuần tự) trong tệp, VÀ
  2. toàn bộ điều phù hợp thoải mái trong bộ nhớ HOẶC có tham chiếu cục bộ trong tệp để các trang nhất định có thể được ánh xạ và các trang khác được ánh xạ. Bằng cách đó, hệ điều hành sử dụng RAM có sẵn để mang lại lợi ích tối đa.
  3. HOẶC nếu nhiều quy trình đang đọc / làm việc trên cùng một tệp, thì mmap () là tuyệt vời vì tất cả các quy trình đều chia sẻ cùng một trang vật lý.

(btw - Tôi yêu mmap () / MapViewOfFile ()).


Điểm hay về truy cập ngẫu nhiên: đây có thể là một trong những điều thúc đẩy nhận thức của tôi.
Ben Collins

1
Tôi không nói rằng tập tin phải vừa vặn với bộ nhớ, chỉ vào không gian địa chỉ. Vì vậy, trên các hệ thống 64 bit, không có lý do gì để không ánh xạ các tệp lớn. HĐH biết cách xử lý điều đó; đó là logic tương tự được sử dụng để hoán đổi nhưng trong trường hợp này không yêu cầu không gian trao đổi bổ sung trên đĩa.
MvG

@MvG: Bạn có hiểu quan điểm về đĩa i / o không? Nếu tệp vừa với không gian địa chỉ nhưng không có bộ nhớ và bạn có quyền truy cập ngẫu nhiên thì bạn có thể có mọi quyền truy cập bản ghi yêu cầu di chuyển và tìm kiếm đầu đĩa, hoặc thao tác trang SSD, sẽ gây ra thảm họa cho hiệu suất.
Tim Cooper

3
Khía cạnh i / o của đĩa phải độc lập với phương thức truy cập. Nếu bạn có quyền truy cập thực sự ngẫu nhiên vào các tệp lớn hơn RAM, cả mmap và tìm kiếm + đọc đều bị ràng buộc đĩa nghiêm trọng. Nếu không cả hai sẽ được hưởng lợi từ bộ nhớ cache. Tôi không thấy kích thước tệp so với kích thước bộ nhớ là một đối số mạnh theo cả hai hướng. Mặt khác, kích thước tệp so với không gian địa chỉ là một đối số rất mạnh, đặc biệt đối với truy cập thực sự ngẫu nhiên.
MvG

Câu trả lời ban đầu của tôi đã có và có điểm này: "toàn bộ điều này phù hợp thoải mái trong bộ nhớ HOẶC có địa phương tham chiếu trong tệp". Vì vậy, điểm thứ 2 giải quyết những gì bạn đang nói.
Tim Cooper

43

mmap là cách nhanh hơn. Bạn có thể viết một điểm chuẩn đơn giản để chứng minh điều đó với chính mình:

char data[0x1000];
std::ifstream in("file.bin");

while (in)
{
  in.read(data, 0x1000);
  // do something with data
}

đấu với:

const int file_size=something;
const int page_size=0x1000;
int off=0;
void *data;

int fd = open("filename.bin", O_RDONLY);

while (off < file_size)
{
  data = mmap(NULL, page_size, PROT_READ, 0, fd, off);
  // do stuff with data
  munmap(data, page_size);
  off += page_size;
}

Rõ ràng, tôi đang bỏ qua các chi tiết (như cách xác định khi nào bạn đến cuối tệp trong trường hợp tệp của bạn không phải là bội số page_size), nhưng thực sự không nên phức tạp hơn thế này .

Nếu bạn có thể, bạn có thể cố gắng chia nhỏ dữ liệu của mình thành nhiều tệp có thể là mmap () - ed toàn bộ thay vì một phần (đơn giản hơn nhiều).

Cách đây vài tháng, tôi đã thực hiện một nửa thực hiện một lớp luồng cửa sổ trượt mmap () - ed cho boost_iostreams, nhưng không ai quan tâm và tôi bận rộn với những thứ khác. Thật không may, tôi đã xóa một kho lưu trữ các dự án cũ chưa hoàn thành vài tuần trước và đó là một trong những nạn nhân :-(

Cập nhật : Tôi cũng nên thêm lời cảnh báo rằng điểm chuẩn này sẽ trông khá khác biệt trong Windows vì Microsoft đã triển khai bộ đệm tệp tiện lợi, thực hiện hầu hết những gì bạn sẽ làm với mmap ở vị trí đầu tiên. Tức là, đối với các tệp được truy cập thường xuyên, bạn chỉ có thể thực hiện std :: ifstream.read () và nó sẽ nhanh như mmap, vì bộ đệm tệp đã thực hiện ánh xạ bộ nhớ cho bạn và nó trong suốt.

Cập nhật cuối cùng : Hãy nhìn xem, mọi người: trên rất nhiều tổ hợp nền tảng khác nhau của hệ điều hành và thư viện tiêu chuẩn và đĩa và phân cấp bộ nhớ, tôi không thể chắc chắn rằng cuộc gọi hệ thống mmap, được xem như một hộp đen, sẽ luôn luôn nhanh hơn đáng kể hơn read. Đó không phải là ý định của tôi, ngay cả khi lời nói của tôi có thể được hiểu theo cách đó. Cuối cùng, quan điểm của tôi là i / o được ánh xạ bộ nhớ thường nhanh hơn i / o dựa trên byte; này vẫn còn đúng . Nếu bạn thấy bằng thực nghiệm rằng không có sự khác biệt giữa hai loại này, thì lời giải thích duy nhất có vẻ hợp lý với tôi là nền tảng của bạn thực hiện ánh xạ bộ nhớ dưới vỏ bọc theo cách có lợi cho việc thực hiện các cuộc gọi đếnread. Cách duy nhất để tuyệt đối chắc chắn rằng bạn đang sử dụng i / o được ánh xạ bộ nhớ theo cách di động là sử dụng mmap. Nếu bạn không quan tâm đến tính di động và bạn có thể dựa vào các đặc điểm cụ thể của nền tảng mục tiêu của mình, thì việc sử dụng readcó thể phù hợp mà không phải hy sinh bất kỳ hiệu suất nào.

Chỉnh sửa để dọn sạch danh sách câu trả lời: @jbl:

cửa sổ trượt mmap nghe có vẻ thú vị. Bạn có thể nói thêm một chút về nó?

Chắc chắn - Tôi đã viết thư viện C ++ cho Git (libgit ++, nếu bạn muốn) và tôi gặp phải một vấn đề tương tự như vậy: Tôi cần có thể mở các tệp lớn (rất lớn) và không có hiệu suất là một con chó hoàn toàn (như nó sẽ được với std::fstream).

Boost::Iostreamsđã có Nguồn mapped_file, nhưng vấn đề là đó là mmapping toàn bộ tệp, giới hạn bạn là 2 ^ (wordsize). Trên máy 32 bit, 4GB không đủ lớn. Không phải là không có lý khi hy vọng có .packcác tệp trong Git trở nên lớn hơn thế nhiều, vì vậy tôi cần đọc tệp theo từng khối mà không cần dùng đến tệp i / o thông thường. Dưới vỏ bọc Boost::Iostreams, tôi đã triển khai một Nguồn, ít nhiều là một cái nhìn khác về sự tương tác giữa std::streambufstd::istream. Bạn cũng có thể thử một cách tiếp cận tương tự bằng cách chỉ kế thừa std::filebufvào một mapped_filebufvà tương tự, kế thừa std::fstreamvào a mapped_fstream. Đó là sự tương tác giữa hai điều đó rất khó để có được đúng. Boost::Iostreams có một số công việc được thực hiện cho bạn và nó cũng cung cấp các móc cho bộ lọc và chuỗi, vì vậy tôi nghĩ sẽ hữu ích hơn khi thực hiện theo cách đó.


3
RE: bộ đệm tập tin mmbed trên Windows. Chính xác: khi bộ đệm tệp được bật, bộ nhớ kernel ánh xạ tệp bạn đang đọc bên trong, đọc vào bộ đệm đó và sao chép lại vào quy trình của bạn. Như thể bộ nhớ của bạn đã tự ánh xạ nó trừ một bước sao chép bổ sung.
Chris Smith

6
Tôi không đồng ý với câu trả lời được chấp nhận, nhưng tôi tin rằng câu trả lời này là sai. Tôi đã làm theo đề xuất của bạn và thử mã của bạn, trên máy Linux 64 bit và mmap () không nhanh hơn triển khai STL. Ngoài ra, về mặt lý thuyết tôi sẽ không mong đợi 'mmap ()' sẽ nhanh hơn (hoặc chậm hơn).
Tim Cooper

3
@Tim Cooper: bạn có thể tìm thấy chủ đề này ( markmail.org/message/, ) quan tâm. Lưu ý hai điều: mmap không được tối ưu hóa đúng cách trong Linux và người ta cũng cần sử dụng madvise trong thử nghiệm của họ để có kết quả tốt nhất.
Ben Collins

9
Ben thân mến: Tôi đã đọc liên kết đó. Nếu 'mmap ()' không nhanh hơn trên Linux và MapViewOfFile () không nhanh hơn trên Windows, thì bạn có thể đưa ra tuyên bố rằng "mmap là cách nhanh hơn" không? Ngoài ra, vì lý do lý thuyết tôi tin rằng mmap () không nhanh hơn cho việc đọc tuần tự - bạn có lời giải thích nào ngược lại không?
Tim Cooper

11
Ben, tại sao phải bận tâm đến mmap()tập tin một trang tại một thời điểm? Nếu a size_tđủ khả năng để giữ kích thước của tệp (rất có thể trên các hệ thống 64 bit), thì chỉ mmap()toàn bộ tệp trong một cuộc gọi.
Steve Emmerson

39

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 , mmapcó thể trông khá giống ma thuật :

  1. 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.
  2. 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.
  3. mmapcho 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 mmapso 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 mmapbạ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ỉ mmaptoà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để mmapbả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 mmapcá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ể mallocmộ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 readcuộ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 mmapcá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ể, mmapcá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_userhiệ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 mmaptrường hợp không có MAP_POPULATE.
  • Bổ sung các copy_to_userphương thức đường dẫn nhanh arch/x86/lib/copy_user_64.S, ví dụ, sử dụng REP MOVQkhi 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 mmapcá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_POPULATEvà 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 mmapreadcá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 madvisehoặc fadvisegọ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ự mmapvà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_POPULATEcá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à.


4
Cảm ơn bạn đã cung cấp một câu trả lời sắc thái hơn cho vấn đề phức tạp này. Có vẻ như rõ ràng với hầu hết mọi người rằng mmap nhanh hơn, trong thực tế, nó thường không phải là trường hợp. Trong các thử nghiệm của tôi, việc truy cập ngẫu nhiên một cơ sở dữ liệu lớn 100 GB với chỉ số trong bộ nhớ hóa ra là nhanh hơn với pread (), mặc dù tôi đã mua một bộ đệm cho mỗi trong số hàng triệu lượt truy cập. Và có vẻ như một nhóm người trong ngành đã quan sát điều tương tự .
Caetano Sauer

5
Vâng, nó phụ thuộc rất nhiều vào kịch bản. Nếu bạn đọc đủ nhỏ và theo thời gian bạn có xu hướng đọc lại nhiều byte giống nhau, mmapsẽ có một lợi thế không thể vượt qua vì nó tránh được chi phí cuộc gọi hạt nhân cố định. Mặt khác, mmapcũng làm tăng áp lực TLB và thực sự làm chậm hơn cho giai đoạn "khởi động" nơi các byte được đọc lần đầu tiên trong quy trình hiện tại (mặc dù chúng vẫn ở trong trang), vì nó có thể làm được nhiều công việc hơn read, ví dụ như "lỗi xung quanh" các trang liền kề ... và đối với các ứng dụng tương tự "khởi động" là tất cả vấn đề! @CaetanoSauer
BeeOnRope

Tôi nghĩ rằng bạn nói "... nhưng lỗi 25 tỷ trang vẫn sẽ không siêu nhanh ..." nên đọc "... nhưng lỗi 25 triệu trang vẫn sẽ không siêu nhanh ..." . Tôi không tích cực 100%, vì vậy đó là lý do tại sao tôi không chỉnh sửa trực tiếp.
Ton van den Heuvel

7

Tôi xin lỗi Ben Collins bị mất mã nguồn cửa sổ trượt của mình. Điều đó thật tuyệt khi có trong Boost.

Có, ánh xạ tập tin nhanh hơn nhiều. Về cơ bản, bạn đang sử dụng hệ thống con bộ nhớ ảo của hệ điều hành để liên kết bộ nhớ với đĩa và ngược lại. Hãy suy nghĩ về nó theo cách này: nếu các nhà phát triển nhân hệ điều hành có thể làm cho nó nhanh hơn họ sẽ làm. Bởi vì làm như vậy làm cho mọi thứ nhanh hơn: cơ sở dữ liệu, thời gian khởi động, thời gian tải chương trình, et cetera.

Cách tiếp cận cửa sổ trượt thực sự không khó vì nhiều trang có thể được ánh xạ cùng một lúc. Vì vậy, kích thước của bản ghi không quan trọng miễn là bản lớn nhất của bất kỳ bản ghi nào sẽ phù hợp với bộ nhớ. Điều quan trọng là quản lý việc giữ sổ sách.

Nếu một bản ghi không bắt đầu trên ranh giới getpagesize (), ánh xạ của bạn phải bắt đầu trên trang trước. Độ dài của vùng được ánh xạ kéo dài từ byte đầu tiên của bản ghi (làm tròn xuống nếu cần đến bội số getpagesize ()) gần nhất đến byte cuối cùng của bản ghi (làm tròn đến bội số gần nhất của getpagesize ()). Khi bạn xử lý xong một bản ghi, bạn có thể hủy ánh xạ () nó và chuyển sang bản ghi tiếp theo.

Tất cả điều này cũng hoạt động tốt trong Windows, bằng cách sử dụng CreatFileMapping () và MapViewOfFile () (và GetSystemInfo () để có được HỆ THỐNG HỆ THỐNG THÔNG TIN TUYỆT VỜI --- không phải HỆ THỐNG HỆ THỐNG --- không phải HỆ THỐNG.


Tôi vừa mới googled và tìm thấy đoạn trích nhỏ này về dw AllocationGranularity - Tôi đã sử dụng dwPageSize và mọi thứ đã bị phá vỡ. Cảm ơn!
wickychicken

4

mmap nên nhanh hơn, nhưng tôi không biết bao nhiêu. Nó rất nhiều phụ thuộc vào mã của bạn. Nếu bạn sử dụng mmap, tốt nhất là mmap toàn bộ tệp cùng một lúc, điều đó sẽ giúp bạn dễ dàng hơn rất nhiều. Một vấn đề tiềm ẩn là nếu tệp của bạn lớn hơn 4GB (hoặc trong thực tế, giới hạn thấp hơn, thường là 2GB), bạn sẽ cần một kiến ​​trúc 64 bit. Vì vậy, nếu bạn đang sử dụng môi trường 32, có lẽ bạn không muốn sử dụng nó.

Đã nói rằng, có thể có một con đường tốt hơn để cải thiện hiệu suất. Bạn nói rằng tệp đầu vào được quét nhiều lần , nếu bạn có thể đọc nó trong một lần và sau đó được thực hiện với nó, điều đó có khả năng nhanh hơn nhiều.


3

Có lẽ bạn nên xử lý trước các tệp, vì vậy mỗi bản ghi nằm trong một tệp riêng biệt (hoặc ít nhất là mỗi tệp có kích thước có thể mmap).

Ngoài ra, bạn có thể thực hiện tất cả các bước xử lý cho mỗi bản ghi trước khi chuyển sang bản ghi tiếp theo không? Có lẽ điều đó sẽ tránh được một số chi phí IO?


3

Tôi đồng ý rằng tập tin mmap'd I / O sẽ là nhanh hơn, nhưng trong khi bạn benchmark mã, nên không phải là ví dụ truy cập được phần nào tối ưu?

Ben Collins đã viết:

char data[0x1000];
std::ifstream in("file.bin");

while (in)
{
    in.read(data, 0x1000);
    // do something with data 
}

Tôi cũng đề nghị thử:

char data[0x1000];
std::ifstream iifle( "file.bin");
std::istream  in( ifile.rdbuf() );

while( in )
{
    in.read( data, 0x1000);
    // do something with data
}

Và hơn thế nữa, bạn cũng có thể thử tạo kích thước bộ đệm có cùng kích thước với một trang của bộ nhớ ảo, trong trường hợp 0x1000 không phải là kích thước của một trang bộ nhớ ảo trên máy của bạn ... Tôi vẫn giữ tệp I / O của IMHO chiến thắng, nhưng điều này sẽ làm cho mọi thứ gần hơn.


2

Theo suy nghĩ của tôi, việc sử dụng mmap () "chỉ" giải phóng gánh nặng cho nhà phát triển khỏi việc phải viết mã bộ nhớ đệm của riêng họ. Trong trường hợp "đọc qua tệp một lần" đơn giản, điều này sẽ không khó (mặc dù như mlbrock chỉ ra rằng bạn vẫn lưu bản sao bộ nhớ vào không gian xử lý), nhưng nếu bạn quay đi quay lại trong tệp hoặc bỏ qua các bit và vv, tôi tin rằng các nhà phát triển kernel có thể đã thực hiện công việc lưu trữ bộ đệm tốt hơn tôi có thể ...


1
Nhiều khả năng bạn có thể làm tốt hơn công việc lưu trữ dữ liệu dành riêng cho ứng dụng của mình so với nhân có thể, hoạt động trên các đoạn có kích thước trang theo cách rất mù (ví dụ: nó chỉ sử dụng lược đồ giả đơn giản để quyết định loại trang nào để đuổi ) - trong khi bạn có thể biết rất nhiều về độ chi tiết của bộ nhớ đệm đúng và cũng có một ý tưởng tốt về các mẫu truy cập trong tương lai. Lợi ích thực sự của mmapbộ nhớ đệm là bạn chỉ cần sử dụng lại bộ đệm trang hiện có đã có sẵn, do đó bạn có được bộ nhớ đó miễn phí và nó cũng có thể được chia sẻ qua các quy trình.
BeeOnRope

2

Tôi nhớ ánh xạ một tập tin lớn chứa cấu trúc cây vào bộ nhớ nhiều năm trước. Tôi đã rất ngạc nhiên bởi tốc độ so với việc tuần tự hóa thông thường liên quan đến rất nhiều công việc trong bộ nhớ, như phân bổ các nút cây và thiết lập các con trỏ. Vì vậy, trên thực tế, tôi đã so sánh một cuộc gọi duy nhất với mmap (hoặc đối tác của nó trên Windows) với nhiều cuộc gọi (MANY) với các cuộc gọi mới của nhà điều hành và nhà xây dựng. Đối với loại nhiệm vụ như vậy, mmap là vô địch so với khử tuần tự. Tất nhiên người ta nên nhìn vào tăng con trỏ di chuyển cho điều này.


Nghe có vẻ giống như một công thức cho thảm họa. Bạn làm gì nếu bố cục đối tượng thay đổi? Nếu bạn có các hàm ảo, tất cả các con trỏ vftbl có thể sẽ sai. Làm thế nào để bạn kiểm soát nơi tập tin được ánh xạ đến? Bạn có thể cung cấp cho nó một địa chỉ, nhưng đó chỉ là một gợi ý và kernel có thể chọn một địa chỉ cơ sở khác.
Jens

Điều này hoạt động hoàn hảo khi bạn có một bố cục cây ổn định và được xác định rõ ràng. Sau đó, bạn có thể truyền mọi thứ vào các cấu trúc có liên quan của mình và theo dõi các con trỏ tệp bên trong bằng cách thêm một phần bù "địa chỉ bắt đầu mmap" mỗi lần. Điều này rất giống với các hệ thống tệp sử dụng inodes và cây thư mục
Mike76

1

Điều này nghe có vẻ như là một trường hợp sử dụng tốt cho đa luồng ... Tôi nghĩ rằng bạn có thể dễ dàng thiết lập một luồng để đọc dữ liệu trong khi các luồng khác xử lý nó. Đó có thể là một cách để tăng đáng kể hiệu suất nhận thức. Chỉ là một ý nghĩ.


Vâng. Tôi đã suy nghĩ về điều đó và có thể sẽ thử nó trong một bản phát hành sau. Bảo lưu duy nhất tôi có là việc xử lý ngắn hơn nhiều so với độ trễ I / O, do đó có thể không có nhiều lợi ích.
jbl

1

Tôi nghĩ rằng điều tuyệt vời nhất về mmap là tiềm năng cho việc đọc không đồng bộ với:

    addr1 = NULL;
    while( size_left > 0 ) {
        r = min(MMAP_SIZE, size_left);
        addr2 = mmap(NULL, r,
            PROT_READ, MAP_FLAGS,
            0, pos);
        if (addr1 != NULL)
        {
            /* process mmap from prev cycle */
            feed_data(ctx, addr1, MMAP_SIZE);
            munmap(addr1, MMAP_SIZE);
        }
        addr1 = addr2;
        size_left -= r;
        pos += r;
    }
    feed_data(ctx, addr1, r);
    munmap(addr1, r);

Vấn đề là tôi không thể tìm thấy MAP_FLAGS phù hợp để đưa ra gợi ý rằng bộ nhớ này sẽ được đồng bộ hóa từ tập tin càng sớm càng tốt. Tôi hy vọng rằng MAP_POPULATE đưa ra gợi ý phù hợp cho mmap (nghĩa là nó sẽ không cố tải tất cả nội dung trước khi quay lại từ cuộc gọi, nhưng sẽ thực hiện điều đó trong async. Với feed_data). Ít nhất là nó cho kết quả tốt hơn với cờ này ngay cả khi hướng dẫn sử dụng nói rằng nó không làm gì nếu không có MAP_PRIVATE kể từ 2.6.23.


1
Bạn muốn posix_madvisevớiWILLNEED cờ cho gợi ý lười biếng để chuẩn bị trước.
ShadowRanger

@ShadowRanger, nghe có vẻ hợp lý. Mặc dù tôi muốn cập nhật trang người dùng để nêu rõ đó posix_madviselà cuộc gọi không đồng bộ. Cũng rất tốt để tham khảo mlockcho những người muốn đợi cho đến khi toàn bộ vùng bộ nhớ trở nên khả dụng mà không có lỗi trang.
ony
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.