Я пытаюсь разобраться в некоторых довольно неутешительных результатах, которые мы получаем для наших приложений HPC. Я написал следующий тест в Visual Studio 2010, перегоняется сущность наших приложений (много независимых, высоких операции арифметической интенсивности):Strange Multithreading Performance
#include "stdafx.h"
#include <math.h>
#include <time.h>
#include <Windows.h>
#include <stdio.h>
#include <memory.h>
#include <process.h>
void makework(void *jnk) {
double tmp = 0;
for(int j=0; j<10000; j++) {
for(int i=0; i<1000000; i++) {
tmp = tmp+(double)i*(double)i;
}
}
*((double *)jnk) = tmp;
_endthread();
}
void spawnthreads(int num) {
HANDLE *hThreads = (HANDLE *)malloc(num*sizeof(HANDLE));
double *junk = (double *)malloc(num*sizeof(double));
printf("Starting %i threads... ", num);
for(int i=0; i<num; i++) {
hThreads[i] = (HANDLE)_beginthread(makework, 0, &junk[i]);
}
int start = GetTickCount();
WaitForMultipleObjects(num, hThreads, TRUE, INFINITE);
int end = GetTickCount();
FILE *fp = fopen("makework.log", "a+");
fprintf(fp, "%i,%.3f\n", num, (double)(end-start)/1000.0);
fclose(fp);
printf("Elapsed time: %.3f seconds\n", (double)(end-start)/1000.0);
free(hThreads);
free(junk);
}
int _tmain(int argc, _TCHAR* argv[])
{
for(int i=1; i<=20; i++) {
spawnthreads(i);
}
return 0;
}
Я делаю ту же самую операцию в каждом потоке, поэтому она должна (в идеале) занимает постоянную ~ 11 секунд, пока я не заполнил физические ядра, а затем, возможно, удвоится, когда начну использовать логические гиперпотоки. Не должно быть проблем с кешем, поскольку переменные цикла и результаты могут вписываться в регистры.
Вот результаты моего эксперимента на двух стендах, как под управлением Windows Server 2008.
машина 1 Dual Xeon X5690 @ 3,47 ГГц - 12 физических ядер, 24 логических ядер, Westmere архитектуры
Starting 1 threads... Elapsed time: 11.575 seconds
Starting 2 threads... Elapsed time: 11.575 seconds
Starting 3 threads... Elapsed time: 11.591 seconds
Starting 4 threads... Elapsed time: 11.684 seconds
Starting 5 threads... Elapsed time: 11.825 seconds
Starting 6 threads... Elapsed time: 12.324 seconds
Starting 7 threads... Elapsed time: 14.992 seconds
Starting 8 threads... Elapsed time: 15.803 seconds
Starting 9 threads... Elapsed time: 16.520 seconds
Starting 10 threads... Elapsed time: 17.098 seconds
Starting 11 threads... Elapsed time: 17.472 seconds
Starting 12 threads... Elapsed time: 17.519 seconds
Starting 13 threads... Elapsed time: 17.395 seconds
Starting 14 threads... Elapsed time: 17.176 seconds
Starting 15 threads... Elapsed time: 16.973 seconds
Starting 16 threads... Elapsed time: 17.144 seconds
Starting 17 threads... Elapsed time: 17.129 seconds
Starting 18 threads... Elapsed time: 17.581 seconds
Starting 19 threads... Elapsed time: 17.769 seconds
Starting 20 threads... Elapsed time: 18.440 seconds
машина 2 Dual Xeon E5-2690 @ 2,90 ГГц - 16 физических ядер, 32 логических ядер, архитектура Sandy Bridge
Starting 1 threads... Elapsed time: 10.249 seconds
Starting 2 threads... Elapsed time: 10.562 seconds
Starting 3 threads... Elapsed time: 10.998 seconds
Starting 4 threads... Elapsed time: 11.232 seconds
Starting 5 threads... Elapsed time: 11.497 seconds
Starting 6 threads... Elapsed time: 11.653 seconds
Starting 7 threads... Elapsed time: 11.700 seconds
Starting 8 threads... Elapsed time: 11.888 seconds
Starting 9 threads... Elapsed time: 12.246 seconds
Starting 10 threads... Elapsed time: 12.605 seconds
Starting 11 threads... Elapsed time: 13.026 seconds
Starting 12 threads... Elapsed time: 13.041 seconds
Starting 13 threads... Elapsed time: 13.182 seconds
Starting 14 threads... Elapsed time: 12.885 seconds
Starting 15 threads... Elapsed time: 13.416 seconds
Starting 16 threads... Elapsed time: 13.011 seconds
Starting 17 threads... Elapsed time: 12.949 seconds
Starting 18 threads... Elapsed time: 13.011 seconds
Starting 19 threads... Elapsed time: 13.166 seconds
Starting 20 threads... Elapsed time: 13.182 seconds
Вот аспекты я нахожу озадачиваю:
Почему время, прошедшее с машиной Westmere остается постоянным сезам около 6 ядер, а затем прыгать внезапно, а затем оставаться в основном постоянными выше 10 потоков? Развертывает ли Windows все потоки в одном процессоре, прежде чем переходить ко второму, так что гиперпоточность начинается без детерминированности после заполнения одного процессора?
Почему время, прошедшее с помощью машины Sandy Bridge, увеличивается в основном линейно с количеством потоков до примерно 12? Двенадцать не кажется значимым числом для меня, учитывая количество ядер.
Любые мысли и предложения по счетчикам процессоров для измерения/способов улучшения моего теста оцениваются. Это проблема архитектуры или проблема с Windows?
Edit:
Как предлагается ниже, компилятор делает некоторые странные вещи, так что я написал свой собственный код сборки, который делает то же самое, что и выше, но оставляет все операции FP на стеке FP, чтобы избежать любой доступ к памяти:
void makework(void *jnk) {
register int i, j;
// register double tmp = 0;
__asm {
fldz // this holds the result on the stack
}
for(j=0; j<10000; j++) {
__asm {
fldz // push i onto the stack: stack = 0, res
}
for(i=0; i<1000000; i++) {
// tmp += (double)i * (double)i;
__asm {
fld st(0) // stack: i, i, res
fld st(0) // stack: i, i, i, res
fmul // stack: i*i, i, res
faddp st(2), st(0) // stack: i, res+i*i
fld1 // stack: 1, i, res+i*i
fadd // stack: i+1, res+i*i
}
}
__asm {
fstp st(0) // pop i off the stack leaving only res in st(0)
}
}
__asm {
mov eax, dword ptr [jnk]
fstp qword ptr [eax]
}
// *((double *)jnk) = tmp;
_endthread();
}
Это компонует как:
013E1002 in al,dx
013E1003 fldz
013E1005 mov ecx,2710h
013E100A lea ebx,[ebx]
013E1010 fldz
013E1012 mov eax,0F4240h
013E1017 fld st(0)
013E1019 fld st(0)
013E101B fmulp st(1),st
013E101D faddp st(2),st
013E101F fld1
013E1021 faddp st(1),st
013E1023 dec eax
013E1024 jne makework+17h (13E1017h)
013E1026 fstp st(0)
013E1028 dec ecx
013E1029 jne makework+10h (13E1010h)
013E102B mov eax,dword ptr [jnk]
013E102E fstp qword ptr [eax]
013E1030 pop ebp
013E1031 jmp dword ptr [__imp___endthread (13E20C0h)]
Результатов для машины-выше:
Starting 1 threads... Elapsed time: 12.589 seconds
Starting 2 threads... Elapsed time: 12.574 seconds
Starting 3 threads... Elapsed time: 12.652 seconds
Starting 4 threads... Elapsed time: 12.682 seconds
Starting 5 threads... Elapsed time: 13.011 seconds
Starting 6 threads... Elapsed time: 13.790 seconds
Starting 7 threads... Elapsed time: 16.411 seconds
Starting 8 threads... Elapsed time: 18.003 seconds
Starting 9 threads... Elapsed time: 19.220 seconds
Starting 10 threads... Elapsed time: 20.124 seconds
Starting 11 threads... Elapsed time: 20.764 seconds
Starting 12 threads... Elapsed time: 20.935 seconds
Starting 13 threads... Elapsed time: 20.748 seconds
Starting 14 threads... Elapsed time: 20.717 seconds
Starting 15 threads... Elapsed time: 20.608 seconds
Starting 16 threads... Elapsed time: 20.685 seconds
Starting 17 threads... Elapsed time: 21.107 seconds
Starting 18 threads... Elapsed time: 21.451 seconds
Starting 19 threads... Elapsed time: 22.043 seconds
Starting 20 threads... Elapsed time: 22.745 seconds
Так что около 9% медленнее с одной нитью, а когда все физические ядра заполнены это (разница между ИНКАМИ е против FLD1 и faddp, возможно?) почти в два раза медленнее (что можно было бы ожидать от гиперпотока). Но загадочный аспект ухудшения производительности, начиная с всего лишь 6 потоков, по-прежнему остается ...
Очень (* очень *) случайная идея: планировщик перетасовывает потоки вокруг без необходимости. Получают ли результаты одинаковые результаты, если вы вручную задаете привязку к процессору для каждого потока? – us2012
Кроме того, вы используете свои потоки с приоритетом в реальном времени? – jxh
Ну, это не проблема планировщика. Вы ожидаете сигнала снова на 12 и 18 потоков. У Xeon действительно есть только 6 ядер. Я бы предположил, что с установкой numa с двумя фишками, вы видите накладные расходы на межсоединение. –