mmap:使用多线程时的性能
mmap: performance when using multithreading
我有一个程序可以对很多文件(> 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
调用的,但如果我删除它或更改 advise 参数,它似乎没有任何改变(或者只是减慢速度)。
- 线程亲和性似乎并不重要。我试图将每个线程限制为特定的核心。我还尝试将线程限制为核心对(超线程)。目前没有结果。
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;
}
一些注意事项:
- 尝试 运行使用
perf stat -ddd <app>
连接您的应用程序并查看上下文切换、cpu-迁移和页面错误数。
- 线程可能在
mmap
和页面错误的内核进程结构中争用 vm_area_struct
。尝试将 MAP_POPULATE
或 MAP_LOCKED
标志传递给 mmap
以最大程度地减少页面错误。或者,尝试仅在主线程中使用 mmap
和 MAP_POPULATE
或 MAP_LOCKED
标志(在这种情况下,您可能希望确保同一 NUMA 节点上的所有线程 运行)。
- 您可能还想尝试使用
MAP_HUGETLB
和 MAP_HUGE_2MB, MAP_HUGE_1GB
标志之一。
- 尝试将线程绑定到与
numactl
相同的 NUMA 节点,以确保线程仅访问本地 NUMA 内存。例如。 numactl --membind=0 --cpunodebind=0 <app>
.
- 在
stop = true
之前锁定互斥量,否则条件变量通知可能会丢失并永远死锁等待线程。
p.is_regular_file()
检查不需要锁定互斥量。
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()
调用引起的进程特定的虚拟内存操作拆分为两个可分离的集合,可以 运行 并行。
我有一个程序可以对很多文件(> 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
调用的,但如果我删除它或更改 advise 参数,它似乎没有任何改变(或者只是减慢速度)。 - 线程亲和性似乎并不重要。我试图将每个线程限制为特定的核心。我还尝试将线程限制为核心对(超线程)。目前没有结果。
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;
}
一些注意事项:
- 尝试 运行使用
perf stat -ddd <app>
连接您的应用程序并查看上下文切换、cpu-迁移和页面错误数。 - 线程可能在
mmap
和页面错误的内核进程结构中争用vm_area_struct
。尝试将MAP_POPULATE
或MAP_LOCKED
标志传递给mmap
以最大程度地减少页面错误。或者,尝试仅在主线程中使用mmap
和MAP_POPULATE
或MAP_LOCKED
标志(在这种情况下,您可能希望确保同一 NUMA 节点上的所有线程 运行)。 - 您可能还想尝试使用
MAP_HUGETLB
和MAP_HUGE_2MB, MAP_HUGE_1GB
标志之一。 - 尝试将线程绑定到与
numactl
相同的 NUMA 节点,以确保线程仅访问本地 NUMA 内存。例如。numactl --membind=0 --cpunodebind=0 <app>
. - 在
stop = true
之前锁定互斥量,否则条件变量通知可能会丢失并永远死锁等待线程。 p.is_regular_file()
检查不需要锁定互斥量。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()
调用引起的进程特定的虚拟内存操作拆分为两个可分离的集合,可以 运行 并行。