【发布时间】:2020-10-30 05:40:02
【问题描述】:
我正在编写路径跟踪器作为编程练习。昨天我终于决定实现多线程——而且效果很好。然而,一旦我将我在main() 中编写的测试代码包装在一个单独的renderer 类中,我注意到性能显着下降。简而言之 - 似乎在main() 之外的任何地方填充std::vector 会导致使用其元素的线程性能更差。我设法用简化的代码隔离并重现了这个问题,但不幸的是我仍然不知道它为什么会发生或如何解决它。
性能下降非常明显且一致:
97 samples - time = 28.154226s, per sample = 0.290250s, per sample/th = 1.741498
99 samples - time = 28.360723s, per sample = 0.286472s, per sample/th = 1.718832
100 samples - time = 29.335468s, per sample = 0.293355s, per sample/th = 1.760128
vs.
98 samples - time = 30.197734s, per sample = 0.308140s, per sample/th = 1.848841
99 samples - time = 30.534240s, per sample = 0.308427s, per sample/th = 1.850560
100 samples - time = 30.786519s, per sample = 0.307865s, per sample/th = 1.847191
我最初在这个问题中发布的代码可以在这里找到:https://github.com/Jacajack/rt/tree/mt_debug 或在编辑历史记录中。
我创建了一个结构foo,它应该模仿我的renderer 类的行为,并负责在其构造函数中初始化路径跟踪上下文。
有趣的是,当我删除foo 的构造函数的主体并改为执行此操作时(直接从main() 初始化contexts):
std::vector<rt::path_tracer> contexts; // Can be on stack or on heap, doesn't matter
foo F(cam, scene, bvh, width, height, render_threads, contexts); // no longer fills `contexts`
contexts.reserve(render_threads);
for (int i = 0; i < render_threads; i++)
contexts.emplace_back(cam, scene, bvh, width, height, 1000 + i);
F.run(render_threads);
性能恢复正常。但是,如果我将这三行包装成一个单独的函数并从这里调用它,那就更糟了。我在这里能看到的唯一模式是
在main() 之外填充contexts 向量会导致问题。
我最初认为这是一个对齐/缓存问题,所以我尝试将 path_tracers 与 Boost 的 aligned_allocator 和 TBB 的 cache_aligned_allocator 对齐,但没有结果。事实证明,即使只有一个线程在运行,这个问题仍然存在。
我怀疑它一定是某种疯狂的编译器优化(我正在使用-O3),尽管这只是一个猜测。您是否知道此类行为的任何可能原因以及可以采取哪些措施来避免这种行为?
这发生在gcc 10.1.0 和clang 10.0.0 上。目前我只使用-O3。
我设法在这个独立示例中重现了类似的问题:
#include <iostream>
#include <thread>
#include <random>
#include <algorithm>
#include <chrono>
#include <iomanip>
struct foo
{
std::mt19937 rng;
std::uniform_real_distribution<float> dist;
std::vector<float> buf;
int cnt = 0;
foo(int seed, int n) :
rng(seed),
dist(0, 1),
buf(n, 0)
{
}
void do_stuff()
{
// Do whatever
for (auto &f : buf)
f = (f + 1) * dist(rng);
cnt++;
}
};
int main()
{
int N = 50000000;
int thread_count = 6;
struct bar
{
std::vector<std::thread> threads;
std::vector<foo> &foos;
bool active = true;
bar(std::vector<foo> &f, int thread_count, int n) :
foos(f)
{
/*
foos.reserve(thread_count);
for (int i = 0; i < thread_count; i++)
foos.emplace_back(1000 + i, n);
//*/
}
void run(int thread_count)
{
auto task = [this](foo &f)
{
while (this->active)
f.do_stuff();
};
threads.reserve(thread_count);
for (int i = 0; i < thread_count; i++)
threads.emplace_back(task, std::ref(foos[i]));
}
};
std::vector<foo> foos;
bar B(foos, thread_count, N);
///*
foos.reserve(thread_count);
for (int i = 0; i < thread_count; i++)
foos.emplace_back(1000 + i, N);
//*/
B.run(thread_count);
std::vector<float> buffer(N, 0);
int samples = 0, last_samples = 0;
// Start time
auto t_start = std::chrono::high_resolution_clock::now();
while (1)
{
last_samples = samples;
samples = 0;
for (auto &f : foos)
{
std::transform(
f.buf.cbegin(), f.buf.cend(),
buffer.begin(),
buffer.begin(),
std::plus<float>()
);
samples += f.cnt;
}
if (samples != last_samples)
{
auto t_now = std::chrono::high_resolution_clock::now();
std::chrono::duration<double> t_total = t_now - t_start;
std::cerr << std::setw(4) << samples << " samples - time = " << std::setw(8) << std::fixed << t_total.count()
<< "s, per sample = " << std::setw(8) << std::fixed << t_total.count() / samples
<< "s, per sample/th = " << std::setw(8) << std::fixed << t_total.count() / samples * thread_count << std::endl;
}
}
}
和结果:
For N = 100000000, thread_count = 6
In main():
196 samples - time = 26.789526s, per sample = 0.136681s, per sample/th = 0.820088
197 samples - time = 27.045646s, per sample = 0.137288s, per sample/th = 0.823725
200 samples - time = 27.312159s, per sample = 0.136561s, per sample/th = 0.819365
vs.
In foo::foo():
193 samples - time = 22.690566s, per sample = 0.117568s, per sample/th = 0.705406
196 samples - time = 22.972403s, per sample = 0.117206s, per sample/th = 0.703237
198 samples - time = 23.257542s, per sample = 0.117462s, per sample/th = 0.704774
200 samples - time = 23.540432s, per sample = 0.117702s, per sample/th = 0.706213
结果似乎与我的路径跟踪器中发生的情况相反,但可见的差异仍然存在。
谢谢
【问题讨论】:
-
你是如何编译你的代码的?您是否启用了编译器优化?如果没有,请按照步骤 1 进行操作。
-
请提供minimal reproducible examples,细节很重要。还包括您用于编译的所有编译器标志
-
最初我启用了
-ffast-math、-march=native、-ftree-vectorize和-O3。现在我只有-O3,它仍然会发生。 -
您还应该包括基准(细节很重要)
-
是的,去吧。目前还没有引用旧代码的答案,如果有人想查看旧代码,它不会被永久删除。
标签: c++ multithreading performance c++11 vector