Я пишу трассировщик пути в качестве упражнения по программированию. Вчера я наконец решил реализовать многопоточность - и это хорошо сработало. Однако, как только я обернул тестовый код, который я написал внутри main()
, в отдельный класс renderer
, я заметил значительное и последовательное падение производительности. Короче говоря, может показаться, что заполнение std::vector
где-либо за пределами main()
приводит к ухудшению работы потоков, использующих его элементы. Мне удалось изолировать и воспроизвести проблему с помощью упрощенного кода, но, к сожалению, я до сих пор не знаю, почему это происходит и что делать, чтобы исправить это.
Падение производительности довольно заметное и последовательное:
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
, которая должна mimi c поведение моего renderer
класса и отвечает за инициализацию контекстов трассировки пути в его конструкторе. Интересно то, что когда я удаляю тело конструктора foo
и вместо этого делаю это (инициализирую contexts
непосредственно из main()
):
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);
производительность возвращается к норме. Но потом, если я вынесу эти три строки в отдельную функцию и вызову ее отсюда, снова станет хуже. Единственная закономерность, которую я здесь вижу, это то, что заполнение вектора contexts
за пределами main()
вызывает проблему.
Сначала я думал, что это проблема с выравниванием / кешированием, поэтому я попытался выровнять path_tracer
s с Boost's 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
It кажется, что результаты противоположны тому, что происходит в моем трассировщике пути, но видимая разница все еще здесь.
Спасибо