C++ Использование `.reserve()` для дополнения `std::vector`s как способ защиты от аннулирования многопоточного кэша и ложного совместного использования
У меня есть программа с общей структурой, показанной ниже. В принципе, у меня есть вектор объектов. Каждый объект имеет векторы-члены, и один из них является вектором структур, которые содержат больше векторов. Благодаря многопоточности объекты обрабатываются параллельно, выполняя вычисления, которые включают в себя значительный доступ и модификацию элементов вектора-члена. К одному объекту одновременно обращается только один поток, и он копируется в стек этого потока для обработки.
Проблема в том, что программе не удается масштабировать до 16 ядер. Я подозреваю и предупреждаю, что проблема может быть в ложном обмене и / или аннулировании кэша. Если это так, то, по-видимому, причина кроется в векторах, выделяющих память слишком близко друг к другу, так как я понимаю, что обе проблемы (в простом выражении) вызваны одновременным доступом к проксимальным адресам памяти различных процессоров. Имеет ли смысл это рассуждение, возможно ли, что это может произойти? Если это так, кажется, что я могу решить эту проблему, добавив векторы-члены, используя.reserve(), чтобы добавить дополнительную емкость, оставляя большие пространства пустой памяти между векторными массивами. Итак, имеет ли все это смысл? Я полностью на обед?
struct str{
vector <float> a; vector <int> b; vector <bool> c; };
class objects{
vector <str> a; vector <int> b; vector <float> c;
//more vectors, etc ...
void DoWork(); //heavy use of vectors
};
main(){
vector <object> objs;
vector <object> p_objs = &objs;
//...make `thread_list` and `attr`
for(int q=0; q<NUM_THREADS; q++)
pthread_create(&thread_list[q], &attr, Consumer, p_objs );
//...
}
void* Consumer(void* argument){
vector <object>* p_objs = (vector <object>*) argument ;
while(1){
index = queued++; //imagine queued is thread-safe global
object obj = (*p_objs)[index]
obj.DoWork();
(*p_objs)[index] = obj;
}
1 ответ
Ну, последний вектор, скопированный в нить 0 objs[0].c
, Первый вектор, скопированный в поток 1 objs[1].a[0].a
, Таким образом, если их два блока распределенных данных занимают одну и ту же строку кэша (64 байта или что бы то ни было на самом деле для этого ЦП), у вас будет ложное совместное использование.
И, конечно, то же самое верно для любых двух задействованных векторов, но только для конкретного примера я сделал вид, что поток 0 запускается первым и выполняет свое распределение до того, как поток 1 начинает выделять, и что распределитель стремится делать последовательные выделения смежными,
reserve()
может помешать частям этого блока, на котором вы фактически действуете, занимать одну и ту же строку кэша. Другим вариантом может быть выделение памяти для каждого потока - если блоки этих векторов выделяются из разных пулов, то они не могут занимать одну и ту же строку, если не занимают пулы.
Если у вас нет распределителей на потоки, проблема может быть в распределении памяти, если DoWork
перераспределяет векторы много. Или это может быть конфликт на любом другом общем ресурсе, используемом DoWork
, По сути, представьте, что каждый поток тратит 1/K своего времени на то, что требует глобального монопольного доступа. Тогда может показаться, что он достаточно хорошо распараллеливается до определенного числа J <= K, и в этот момент получение эксклюзивного доступа значительно сказывается на ускорении, поскольку ядра тратят значительную часть времени простоя. За исключением K ядер, с дополнительными ядрами улучшения практически не происходит, потому что общий ресурс не может работать быстрее.
В абсурдном конце этого, представьте себе некоторую работу, которая тратит 1/K своего времени, удерживая глобальную блокировку, и (K-1)/K своего времени, ожидая ввода-вывода. Тогда проблема кажется смущающей параллелью почти до K потоков (независимо от количества ядер), после чего она останавливается.
Так что не сосредотачивайтесь на ложном обмене, пока вы не исключите возможность настоящего обмена;-)