2013-09-13 2 views
2

Я пытаюсь разобраться в некоторых довольно неутешительных результатах, которые мы получаем для наших приложений 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 потоков, по-прежнему остается ...

+1

Очень (* очень *) случайная идея: планировщик перетасовывает потоки вокруг без необходимости. Получают ли результаты одинаковые результаты, если вы вручную задаете привязку к процессору для каждого потока? – us2012

+0

Кроме того, вы используете свои потоки с приоритетом в реальном времени? – jxh

+0

Ну, это не проблема планировщика. Вы ожидаете сигнала снова на 12 и 18 потоков. У Xeon действительно есть только 6 ядер. Я бы предположил, что с установкой numa с двумя фишками, вы видите накладные расходы на межсоединение. –

ответ

1

Теперь, чтобы быть полностью хромым и ответить на мой собственный вопрос - это, как представляется, планировщик, как предлагал @ us2012. Я жёстко сродства маски для заполнения физических ядра, а затем переключиться на hyperthreaded ядер:

void spawnthreads(int num) { 
    ULONG_PTR masks[] = { // for my system; YMMV 
     0x1, 0x4, 0x10, 0x40, 0x100, 0x400, 0x1000, 0x4000, 0x10000, 0x40000, 
     0x100000, 0x400000, 0x2, 0x8, 0x20, 0x80, 0x200, 0x800, 0x2000, 0x8000}; 
    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]); 
     SetThreadAffinityMask(hThreads[i], masks[i]); 
    } 
    int start = GetTickCount(); 
    WaitForMultipleObjects(num, hThreads, TRUE, INFINITE); 
    int end = GetTickCount(); 
    FILE *fp = fopen("makework.log", "a+"); 
    fprintf(fp, "%i,%.3f,%f\n", num, (double)(end-start)/1000.0, junk[0]); 
    fclose(fp); 
    printf("Elapsed time: %.3f seconds\n", (double)(end-start)/1000.0); 
    free(hThreads); 
} 

и получить

Starting 1 threads... Elapsed time: 12.558 seconds 
Starting 2 threads... Elapsed time: 12.558 seconds 
Starting 3 threads... Elapsed time: 12.589 seconds 
Starting 4 threads... Elapsed time: 12.652 seconds 
Starting 5 threads... Elapsed time: 12.621 seconds 
Starting 6 threads... Elapsed time: 12.777 seconds 
Starting 7 threads... Elapsed time: 12.636 seconds 
Starting 8 threads... Elapsed time: 12.886 seconds 
Starting 9 threads... Elapsed time: 13.057 seconds 
Starting 10 threads... Elapsed time: 12.714 seconds 
Starting 11 threads... Elapsed time: 12.777 seconds 
Starting 12 threads... Elapsed time: 12.668 seconds 
Starting 13 threads... Elapsed time: 26.489 seconds 
Starting 14 threads... Elapsed time: 26.505 seconds 
Starting 15 threads... Elapsed time: 26.505 seconds 
Starting 16 threads... Elapsed time: 26.489 seconds 
Starting 17 threads... Elapsed time: 26.489 seconds 
Starting 18 threads... Elapsed time: 26.676 seconds 
Starting 19 threads... Elapsed time: 26.770 seconds 
Starting 20 threads... Elapsed time: 26.489 seconds 

, который, как ожидалось. Теперь вопрос в том, какие настройки ОС можно изменить, чтобы сделать это ближе к поведению по умолчанию, поскольку большая часть нашего кода написана в MATLAB ...

1

На моем ноутбуке с 2 физическими ядрами и 4 логических ядер, я получаю:

<br> 
Starting 1 threads... Elapsed time: 11.638 seconds<br> 
Starting 2 threads... Elapsed time: 12.418 seconds<br> 
Starting 3 threads... Elapsed time: 13.556 seconds<br> 
Starting 4 threads... Elapsed time: 14.929 seconds<br> 
Starting 5 threads... Elapsed time: 20.811 seconds<br> 
Starting 6 threads... Elapsed time: 22.776 seconds<br> 
Starting 7 threads... Elapsed time: 27.160 seconds<br> 
Starting 8 threads... Elapsed time: 30.249 seconds<br> 

Это показывает деградацию, как только у нас есть более чем 1 нить.

Я подозреваю, что причина в том, что функция makework() делает доступ к памяти. Вы можете увидеть это в Visual Studio 2010, установив точку останова в 1-й строке _tmain(). Когда вы нажмете точку останова, нажмите Ctrl-Alt-D, чтобы увидеть окно разборки. В любом месте, где вы видите имя регистра в скобках (например, [esp]), это доступ к памяти. Полоса пропускания кэша уровня 1 на процессоре насыщается. Вы можете проверить эту теорию с помощью модифицированного makework();

void makework(void *jnk) { 
    double tmp = 0; 
    volatile double *p; 
    int i; 
    int j; 
    p=(double*)jnk; 

    for(j=0; j<100000000; j++) { 
     for(i=0; i<100; i++) { 
      tmp = tmp+(double)i*(double)i; 
     } 
     *p=tmp; 
    } 
    *p = tmp; 
    _endthread(); 
} 

Он делает то же количество вычислений, но с дополнительной записи в память, брошенной на каждые 100 итераций. На моем ноутбуке представлены следующие результаты:

Starting 1 threads... Elapsed time: 11.684 seconds<br> 
Starting 2 threads... Elapsed time: 13.760 seconds<br> 
Starting 3 threads... Elapsed time: 14.445 seconds<br> 
Starting 4 threads... Elapsed time: 17.519 seconds<br> 
Starting 5 threads... Elapsed time: 23.369 seconds<br> 
Starting 6 threads... Elapsed time: 25.491 seconds<br> 
Starting 7 threads... Elapsed time: 30.155 seconds<br> 
Starting 8 threads... Elapsed time: 34.460 seconds<br> 

Который показывает, что доступ к памяти может иметь результат. Я попробовал различные параметры компилятора VS2010, чтобы увидеть, могу ли я получить makework(), чтобы не иметь доступа к памяти, но не повезло. Чтобы по-настоящему изучить исходную производительность ядра процессора и количество активных потоков, я подозреваю, что нам нужно будет закодировать makework() в ассемблере.

+0

yep, я согласен с этим. Если в программе столько потоков, сколько есть ядер, и не работает так быстро, как ожидалось, то это верный признак того, что архитектура памяти борется (предполагая, что остальная часть ОС и других приложений не пытается сделать слишком много в то же время). Плохая производительность памяти была узким местом в течение десятилетий ... – bazza

+0

Хорошо, я проведу разборку и отредактирую пост. Если это насыщает пропускную способность кеш-памяти L1, это означает, что кеш держится на Sandy Bridge, чем Westmere (по крайней мере, в этом случае)? Или, может быть, это слишком простой вывод, и ответ - это страшный «это зависит ...» – Andrew

+0

Ну, это зависит :) Но подсистема памяти Sandy Bridge должна быть намного выше, чем Westmere (второй порт загрузки, например, и другие лакомства).Что могло бы пойти не так, так это то, что компилятор может хранить i, j в стеке (обозначается вашим [esp]) - попробуйте определить их с помощью ключевого слова register – Leeor

1

(Возможное объяснение) Вы проверили фоновые действия на этих машинах? Может случиться так, что ОС не сможет полностью посвятить все свои ядра вам. На вашей машине 1 значительный рост начинается, когда вы начинаете занимать более половины ядер. Вы можете конкурировать за ресурсы с чем-то другим.

Вы также можете проверить наличие ограничений и политики домена на своем компьютере/учетной записи, которые не позволяют захватить все доступные ресурсы.

+0

Я смотрю в диспетчере задач, что процессор% идет от ~ 1-2% с шагом 3 или 4%, когда каждая нить запускается и восстанавливается из журналов PerfMon, показывает, что это приложение работает только один. Кроме того, это машины для skunkworks, которые ускользнули от железа (несколько). – Andrew

+0

Кажется, что ваш английский не очень хорош. Я не могу понять ваше письмо. Если общий процессор не достигает 100%, это определенно проблема ИТ-политики. Не аппаратное обеспечение, ОС, ваша программа и т. Д. –

+0

Я использую максимум 20 потоков из 24 или 32 логических процессоров, поэтому теоретическая максимальная загрузка процессора составляет 80% или 62%. – Andrew

0

Хорошо, теперь мы исключили теорию насыщения памяти (хотя - x87? Ouch, не ожидайте большой производительности там. Попробуйте переключиться на SSE/AVX, если вы можете жить с тем, что они предоставляют). Масштабирование ядра должно по-прежнему иметь смысл, давайте посмотрим на модели процессора, которые вы использовали:

Вы можете подтвердить, что это правильные модели?

Intel® Xeon® Processor X5690 (12M Cache, 3.46 GHz, 6.40 GT/s Intel® QPI) 

http://ark.intel.com/products/52576

Intel® Xeon® Processor E5-2690 (20M Cache, 2.90 GHz, 8.00 GT/s Intel® QPI) 

http://ark.intel.com/products/64596/

если да, то первый действительно имеет 6 физических ядра (12 логических), а второй имеет 8 физических ядер (16 логических). Подумайте об этом. Я не думаю, что вы можете получить более высокий показатель ядра в одном сокете в этих поколениях, поэтому он имеет смысл, и он идеально подходит для ваших номеров.

Редактировать: В многопроцессорной системе ОС может предпочесть один разъем, в то время как логических ядер все еще доступны.Это может зависеть от точной версии, но для выигрыша сервера 2008, есть интересный комментарий здесь - http://blogs.technet.com/b/matthts/archive/2012/10/14/windows-server-sockets-logical-processors-symmetric-multi-threading.aspx

процитировать:

When the OS boots it starts with socket 1 and enumerates all logical processors: 

    on socket 1 it enumerates logical processors 1-20 
    on socket 2 it enumerates logical processors 21-40 
    on socket 3 it enumerates logical processors 41-60 
    on socket 4 it would see 61-64 

Если это порядок операционная система пробуждается темы, возможно, SMT пинки в прежде чем перетекать во второй разъем

+0

Да, это модели процессоров, но это обе системы _дуального процессора, поэтому на самом деле есть 12/16 физических ядер. Что касается x87, если была инструкция «float in the pool», которая была бы достаточной для этого упражнения ... :) – Andrew

+0

Пропустил это. Добавлено еще одно предположение – Leeor