Код выполняется в 6 раз медленнее с 2 потоками, чем с 1
Оригинальная проблема:
Поэтому я написал код для экспериментов с потоками и тестирования.
Код должен создать несколько чисел, а затем найти среднее из этих чисел.
Я думаю, что проще показать вам, что у меня есть. Я ожидал с двумя потоками, что код будет работать примерно в 2 раза быстрее. Измеряя его секундомером, я думаю, что он работает примерно в 6 раз медленнее! РЕДАКТИРОВАТЬ: Теперь, используя компьютер и функцию часов (), чтобы узнать время.
void findmean(std::vector<double>*, std::size_t, std::size_t, double*);
int main(int argn, char** argv)
{
// Program entry point
std::cout << "Generating data..." << std::endl;
// Create a vector containing many variables
std::vector<double> data;
for(uint32_t i = 1; i <= 1024 * 1024 * 128; i ++) data.push_back(i);
// Calculate mean using 1 core
double mean = 0;
std::cout << "Calculating mean, 1 Thread..." << std::endl;
findmean(&data, 0, data.size(), &mean);
mean /= (double)data.size();
// Print result
std::cout << " Mean=" << mean << std::endl;
// Repeat, using two threads
std::vector<std::thread> thread;
std::vector<double> result;
result.push_back(0.0);
result.push_back(0.0);
std::cout << "Calculating mean, 2 Threads..." << std::endl;
// Run threads
uint32_t halfsize = data.size() / 2;
uint32_t A = 0;
uint32_t B, C, D;
// Split the data into two blocks
if(data.size() % 2 == 0)
{
B = C = D = halfsize;
}
else if(data.size() % 2 == 1)
{
B = C = halfsize;
D = hsz + 1;
}
// Run with two threads
thread.push_back(std::thread(findmean, &data, A, B, &(result[0])));
thread.push_back(std::thread(findmean, &data, C, D , &(result[1])));
// Join threads
thread[0].join();
thread[1].join();
// Calculate result
mean = result[0] + result[1];
mean /= (double)data.size();
// Print result
std::cout << " Mean=" << mean << std::endl;
// Return
return EXIT_SUCCESS;
}
void findmean(std::vector<double>* datavec, std::size_t start, std::size_t length, double* result)
{
for(uint32_t i = 0; i < length; i ++) {
*result += (*datavec).at(start + i);
}
}
Я не думаю, что этот код совершенно замечательный, если бы вы могли предложить способы его улучшения, я был бы также благодарен за это.
Переменная переменная:
Несколько человек предложили сделать локальную переменную для функции findmean. Вот что я сделал:
void findmean(std::vector<double>* datavec, std::size_t start, std::size_t length, double* result)
{
register double holding = *result;
for(uint32_t i = 0; i < length; i ++) {
holding += (*datavec).at(start + i);
}
*result = holding;
}
Теперь я могу сообщить: код выполняется почти с тем же временем выполнения, что и с одним потоком. Это большое улучшение в 6 раз, но наверняка должен быть способ сделать это почти вдвое быстрее?
Переменная регистра и оптимизация O2:
Я настроил оптимизацию на "O2" - я создам таблицу с результатами.
Результаты на данный момент:
Исходный код без оптимизации или переменной регистра: 1 поток: 4,98 секунды, 2 потока: 29,59 секунды
Код с добавленной регистровой переменной: 1 поток: 4,76 секунды, 2 поток: 4,76 секунды
С переменной reg и оптимизацией -O2: 1 поток: 0,43 секунды, 2 темы: 0,6 секунды 2 темы теперь медленнее?
С предложением Деймона, который должен был поместить большой блок памяти между двумя переменными результата: 1 поток: 0,42 секунды, 2 поток: 0,64 секунды
С предложением TAS использовать итераторы для доступа к содержимому вектора: 1 поток: 0,38 секунды, 2 поток: 0,56 секунды
То же, что и выше для Core i7 920 (одноканальная память 4 ГБ): 1 поток: 0,31 секунды, 2 поток: 0,56 секунды
То же, что и выше для Core i7 920 (двухканальная память 2x2 ГБ): 1 поток: 0,31 секунды, 2 поток: 0,35 секунды
4 ответа
Почему 2 потока в 6 раз медленнее, чем 1 поток?
Вы попали под неудачный случай ложного обмена.
После избавления от ложного обмена, почему 2 потока не быстрее, чем 1 поток?
Вы ограничены пропускной способностью вашей памяти.
Ложный обмен:
Проблема здесь в том, что каждый поток обращается к result
переменная в смежных местах памяти. Скорее всего, они попадают на одну и ту же кеш-линию, поэтому каждый раз, когда поток обращается к ней, он отскакивает кеш-линию между ядрами.
Каждый поток выполняет этот цикл:
for(uint32_t i = 0; i < length; i ++) {
*result += (*datavec).at(start + i);
}
И вы можете видеть, что result
к переменной обращаются очень часто (каждая итерация). Таким образом, на каждой итерации потоки борются за одну и ту же кешлайн, которая содержит оба result
,
Обычно компилятор должен ставить *result
в регистр, тем самым удаляя постоянный доступ к этой ячейке памяти. Но так как вы никогда не включали оптимизацию, вполне вероятно, что компилятор действительно все еще обращается к области памяти и, таким образом, подвергается штрафам за ложное совместное использование на каждой итерации цикла.
Пропускная способность памяти:
После того, как вы устранили ложное совместное использование и избавились от 6-кратного замедления, причина, по которой вы не получаете улучшения, заключается в том, что вы максимально увеличили пропускную способность своей памяти.
Конечно, ваш процессор может иметь 4 ядра, но все они имеют одинаковую пропускную способность памяти. Ваша конкретная задача суммирования массива делает очень мало (вычислительную) работу для каждого доступа к памяти. Одного потока уже достаточно, чтобы максимально увеличить пропускную способность вашей памяти. Поэтому посещение большего количества потоков вряд ли принесет вам много улучшений.
Короче говоря, нет, вы не сможете значительно ускорить суммирование массива, добавив в него больше потоков.
Как указано в других ответах, вы видите ложное совместное использование переменной результата, но есть и еще одно место, где это происходит. std::vector<T>::at()
функция (а также std::vector<T>::operator[]()
) доступ к длине вектора на каждом элементе доступа. Чтобы избежать этого, вы должны перейти на использование итераторов. Кроме того, используя std::accumulate()
позволит вам воспользоваться преимуществами оптимизации в стандартной реализации библиотеки, которую вы используете.
Вот соответствующие части кода:
thread.push_back(std::thread(findmean, std::begin(data)+A, std::begin(data)+B, &(result[0])));
thread.push_back(std::thread(findmean, std::begin(data)+B, std::end(data), &(result[1])));
а также
void findmean(std::vector<double>::const_iterator start, std::vector<double>::const_iterator end, double* result)
{
*result = std::accumulate(start, end, 0.0);
}
Это последовательно дает мне лучшую производительность для двух потоков на моем 32-битном нетбуке.
Больше тем не значит быстрее! При создании потоков и переключении контекста возникают накладные расходы, даже аппаратное обеспечение, на котором выполняется этот код, влияет на результаты. Для такой простой работы, как эта, лучше, наверное, один поток.
Вероятно, это связано с тем, что стоимость запуска и ожидания двух потоков намного больше, чем вычисление результата в одном цикле. Ваш размер данных составляет 128 МБ, что не достаточно для современных процессоров для обработки в один цикл.