mmap:使用多线程时的性能

Mar*_*vič 6 linux multithreading mmap

我有一个程序可以对大量文件(> 10 000)执行一些操作。它产生 N 个工作线程,每个线程映射一些文件,做一些工作并映射它。

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

一些注意事项:

  • 仅当我使用 mmap()/munmap() 时才会发生这种情况。我尝试用 fopen()/fread() 替换它,效果很好。但由于 mmap()/munmap() 附带了第 3 方库,我想以其原始形式使用它。
  • madvise() 被调用,MADV_SEQUENTIAL但如果我删除它或更改 suggest 参数,它似乎不会改变任何东西(或者只是减慢速度)。
  • 线程亲和力似乎并不重要。我试图将每个线程限制为特定的核心。我还尝试将线程限制为核心对(超线程)。到目前为止还没有结果。
  • 即使在这两种情况下,报告的负载htop似乎也是相同的。

所以我的问题是:

  • 在多线程环境中使用 mmap() 时,有什么我不知道的吗?
  • 如果是这样,为什么 2 个进程具有更好的性能?

编辑:

  • 正如评论中指出的,它在具有 2xCPU 的服务器上运行。我可能应该尝试设置线程亲和性,使其始终在同一个 CPU 上运行,但我想我已经尝试过,但它不起作用。
  • 这是一段代码,我可以用它重现与我的生产软件相同的问题。
#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;
}

Run Code Online (Sandbox Code Playgroud)

And*_*nle 7

在多线程环境中使用时有什么mmap()我不知道的吗?

是的。 mmap()需要大量的虚拟内存操作 - 在某些地方有效地单线程处理您的进程。根据Linus Torvalds 的这篇文章:

...使用虚拟内存映射玩游戏本身就非常昂贵。它有许多非常现实的缺点,人们往往会忽略这些缺点,因为内存复制被认为是非常慢的事情,有时优化该副本被视为明显的改进。

mmap 的缺点:

  • 相当明显的安装和拆卸成本。我的意思是引人注目的。就像遵循页表来干净地取消所有内容的映射一样。它是用于维护所有映射列表的簿记。这是取消映射后需要的 TLB 刷新。

  • 页面错误的代价是昂贵的。这就是映射的填充方式,而且速度相当慢。

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

因此,映射文件所需的虚拟内存操作不仅成本高昂,而且实际上无法并行完成 - 内核必须跟踪的只有一块实际物理内存,并且多个线程无法并行化对一个内存的更改。进程的虚拟地址空间。

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

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

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

据,直到...为止

如果是这样,为什么 2 个进程具有更好的性能?

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

  • @MaximEgorushkin 同意,但这是我所知道的最好的权威来源,它清楚而简洁地总结了“mmap()”的真正成本。在只读取一次的文件上使用 mmap() 可能是 mmap() 的最糟糕的用例,至少在性能方面是如此。我个人认为 mmap() 主要是一个可以极大地简化代码的函数,并且具有较小但实际的性能**成本**。 (2认同)