mmap:使用多线程时的性能

mmap: performance when using multithreading

我有一个程序可以对很多文件(> 10 000)执行一些操作。它生成 N 个工作线程,每个线程都映射一些文件,做一些工作并将其映射。

我现在面临的问题是,每当我只使用 1 个进程和 N 个工作线程时,它的性能比生成 2 个进程(每个进程都有 N/2 个工作线程)更差。我可以在 iotop 中看到这一点,因为 1 个进程+N 个线程仅使用大约 75% 的磁盘带宽,而 2 个进程+N/2 个线程使用全部带宽。

一些注意事项:

所以我的问题是:

编辑:

#include <condition_variable>
#include <deque>
#include <filesystem>
#include <iostream>
#include <mutex>
#include <thread>
#include <vector>

#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>

#ifndef WORKERS
#define WORKERS 16
#endif

bool stop = false;
std::mutex queue_mutex;
std::condition_variable queue_cv;

std::pair<const std::uint8_t*, std::size_t> map_file(const std::string& file_path)
{
    int fd = open(file_path.data(), O_RDONLY);
    if (fd != -1)
    {
        auto dir_ent = std::filesystem::directory_entry{file_path.data()};
        if (dir_ent.is_regular_file())
        {
            auto size = dir_ent.file_size();
            auto data = mmap(nullptr, size, PROT_READ, MAP_PRIVATE, fd, 0);
            madvise(data, size, MADV_SEQUENTIAL);
            close(fd);
            return { reinterpret_cast<const std::uint8_t*>(data), size };
        }

        close(fd);
    }

    return { nullptr, 0 };
}

void unmap_file(const std::uint8_t* data, std::size_t size)
{
    munmap((void*)data, size);
}

int main(int argc, char* argv[])
{
    std::deque<std::string> queue;

    std::vector<std::thread> threads;
    for (std::size_t i = 0; i < WORKERS; ++i)
    {
        threads.emplace_back(
            [&]() {
                std::string path;

                while (true)
                {
                    {
                        std::unique_lock<std::mutex> lock(queue_mutex);
                        while (!stop && queue.empty())
                            queue_cv.wait(lock);
                        if (stop && queue.empty())
                            return;
                        path = queue.front();
                        queue.pop_front();
                    }

                    auto [data, size] = map_file(path);
                    std::uint8_t b = 0;
                    for (auto itr = data; itr < data + size; ++itr)
                        b ^= *itr;
                    unmap_file(data, size);

                    std::cout << (int)b << std::endl;
                }
            }
        );
    }

    for (auto& p : std::filesystem::recursive_directory_iterator{argv[1]})
    {
        std::unique_lock<std::mutex> lock(queue_mutex);
        if (p.is_regular_file())
        {
            queue.push_back(p.path().native());
            queue_cv.notify_one();
        }
    }

    stop = true;
    queue_cv.notify_all();

    for (auto& t : threads)
        t.join();

    return 0;
}

一些注意事项:

  1. 尝试 运行使用 perf stat -ddd <app> 连接您的应用程序并查看上下文切换、cpu-迁移和页面错误数。
  2. 线程可能在 mmap 和页面错误的内核进程结构中争用 vm_area_struct。尝试将 MAP_POPULATEMAP_LOCKED 标志传递给 mmap 以最大程度地减少页面错误。或者,尝试仅在主线程中使用 mmapMAP_POPULATEMAP_LOCKED 标志(在这种情况下,您可能希望确保同一 NUMA 节点上的所有线程 运行)。
  3. 您可能还想尝试使用 MAP_HUGETLBMAP_HUGE_2MB, MAP_HUGE_1GB 标志之一。
  4. 尝试将线程绑定到与 numactl 相同的 NUMA 节点,以确保线程仅访问本地 NUMA 内存。例如。 numactl --membind=0 --cpunodebind=0 <app>.
  5. stop = true 之前锁定互斥量,否则条件变量通知可能会丢失并永远死锁等待线程。
  6. p.is_regular_file() 检查不需要锁定互斥量。
  7. std::deque 可以替换为 std::list 并使用 splice 推入和弹出元素以最小化互斥量被锁定的时间。

Is there anything about mmap() I am not aware of when used in multithreaded environment?

是的。 mmap() 需要大量的虚拟内存操作 - 在某些地方有效地单线程处理您的进程。每 this post from one Linus Torvalds:

... playing games with the virtual memory mapping is very expensive in itself. It has a number of quite real disadvantages that people tend to ignore because memory copying is seen as something very slow, and sometimes optimizing that copy away is seen as an obvious improvment.

Downsides to mmap:

  • quite noticeable setup and teardown costs. And I mean noticeable. It's things like following the page tables to unmap everything cleanly. It's the book-keeping for maintaining a list of all the mappings. It's The TLB flush needed after unmapping stuff.

  • page faulting is expensive. That's how the mapping gets populated, and it's quite slow.

请注意,上面的大部分内容也必须在整个机器上是单线程的,例如物理内存的实际映射。

所以映射文件所需的虚拟内存操作不仅代价高昂,而且它们真的不能并行完成 - 内核必须跟踪的只有一大块实际物理内存,而多个线程不能' t 并行更改进程的虚拟地址 space.

您几乎肯定会获得更好的性能,为每个文件重用内存缓冲区,其中每个缓冲区创建 一次 并且足够大以容纳读入的任何文件,然后使用低级 POSIX read() 调用从文件中读取。您可能想尝试使用页面对齐缓冲区并通过使用 O_DIRECT 标志(Linux-特定)调用 open() 来绕过页面缓存,因为您显然永远不会重新使用直接 IO读取任何数据和任何缓存都是对内存和 CPU 周期的浪费。

重用缓冲区也完全消除了任何 munmap()delete/free().

不过,您必须管理缓冲区。也许用 N 个预先创建的缓冲区预填充队列,并在完成文件后将缓冲区返回到队列?

至于

If so, why do 2 processes have better performance?

两个进程的使用将由 mmap() 调用引起的进程特定的虚拟内存操作拆分为两个可分离的集合,可以 运行 并行。