разница во времени процессора для двух одинаковых линий - PullRequest
5 голосов
/ 07 июля 2011

В моей программе есть цикл while, где IterZNext, IterZ являются указателями на узлы в списке. Узлы в списке имеют тип struct с полем с именем «Index».

double xx = 20.0;
double yy = 10000.0;
double zz;      
while (IterZNext!=NULL && NextIndex<=NewIndex)
{
    IterZ=IterZNext;
    IterZNext = IterZ->Next;
    if (IterZNext!=NULL)
    {
        zz = xx + yy;
                NextIndex1 = IterZNext->Index; // line (*)
        NextIndex = IterZNext->Index;  // line (**)
        IterZNext->Index;
    }
}

Когда я профилировал свою программу, я нашел строку (*)

NextIndex1 = IterZNext->Index;

потребляет большую часть процессорного времени (2,193 с), а линия (**)

NextIndex = IterZNext->Index;

, что почти совпадает со строкой (*), использует только 0,093 с. Я использовал Intel VTune Amplifier, чтобы увидеть сборку этих двух линий, которая выглядит следующим образом:

Address Line    Assembly                   CPU Time Instructions Retired
Line (*):
0x1666  561 mov eax, dword ptr [ebp-0x44]   0.015s  50,000,000
0x1669  561 mov ecx, dword ptr [eax+0x8]        
0x166c  561 mov dword ptr [ebp-0x68], ecx   2.178s  1,614,000,000

Line (**):
0x166f  562 mov byte ptr [ebp-0x155], 0x1   0.039s  80,000,000
0x1676  562 mov eax, dword ptr [ebp-0x44]   0.027s  44,000,000
0x1679  562 mov ecx, dword ptr [eax+0x8]        
0x167c  562 mov dword ptr [ebp-0x5c], ecx   0.026s  94,000,000

Если я изменю порядок строки () и строки (*), то программа изменится на

double xx = 20.0;
double yy = 10000.0;
double zz;      
while (IterZNext!=NULL && NextIndex<=NewIndex)
{
    IterZ=IterZNext;
    IterZNext = IterZ->Next;
    if (IterZNext!=NULL)
    {
        zz = xx + yy;
                NextIndex = IterZNext->Index;  // line (**)
                NextIndex1 = IterZNext->Index; // line (*)
        IterZNext->Index;
    }
}

и результат сборки меняется на

Address Line    Assembly    CPU Time    Instructions Retired
Line (**):
0x1666  560 mov byte ptr [ebp-0x155], 0x1   0.044s  84,000,000
0x166d  560 mov eax, dword ptr [ebp-0x44]   0.006s  2,000,000
0x1670  560 mov ecx, dword ptr [eax+0x8]    0.001s  4,000,000
0x1673  560 mov dword ptr [ebp-0x5c], ecx   1.193s  1,536,000,000

Line (*):
0x1676  561 mov eax, dword ptr [ebp-0x44]   0.052s  128,000,000
0x1679  561 mov ecx, dword ptr [eax+0x8]        
0x167c  561 mov dword ptr [ebp-0x68], ecx   0.034s  112,000,000

В этом случае строка (*) использует большую часть времени ЦП (1,245 с), а строка () использует только 0,086 с.

Может кто-нибудь сказать мне: (1) Почему первое задание занимает так много времени? Обратите внимание, что строка zz = xx + yy использует только 0,058 с. Это связано с отсутствием кэша? поскольку все узлы в списке генерируются динамически. (2) Почему между этими двумя строками огромная разница во времени процессора?

Спасибо!

Ответы [ 3 ]

6 голосов
/ 08 июля 2011

Все современные ЦП являются суперскалярными и не по порядку - это означает, что инструкции на самом деле не выполняются в порядке сборки, и на самом деле не существует такого понятия, как текущий ПК - существует много десятков команд в полет и выполнение сразу.

Следовательно, любая информация выборки, которую сообщает ЦП, является лишь грубой областью, которую ЦП выполнял - он выполнял указанную инструкцию, когда прерывание выборки сработало; но он также выполнял все остальные в полете!

Однако люди привыкли (и ожидают), что инструменты профилирования сообщают им точно , какую отдельную инструкцию выполняет CPU в данный момент - поэтому, когда прерывание выборки запускает, CPU по существу выбирает одну из многих активных инструкций быть «текущим».

5 голосов
/ 08 июля 2011

Кэширование строки процессора , вероятно, причина.доступ к [ebp-0x5c] также приводит к попаданию в кеш [ebp-0x68], который затем будет извлечен намного быстрее (для второго случая, и наоборот для первого).

0 голосов
/ 05 декабря 2012

Это определенно связано с отсутствием кэша. Чем больше промах, тем больше ухудшение производительности будет введено процессором. На самом деле в современном мире процессор работает намного быстрее, чем память. Если в настоящее время процессор может иметь тактовую частоту около 4 ГГц, память все еще работает с частотой ~ 0,3 ГГц. Это большой разрыв в производительности, который все еще продолжает расти. Введение кэша было вызвано желанием скрыть этот пробел. Без использования кеша современный процессор будет тратить огромное количество времени на ожидание данных из памяти и ничего не делать в это время. В дополнение к разрыву в производительности каждый доступ к памяти создает дополнительные задержки, связанные с возможным параллелизмом на шине памяти с другими процессорами и устройствами DMA, а также время, необходимое для обработки и маршрутизации запросов на доступ к памяти на стороне логики управления памятью процессора (проверка кэшей). на всех уровнях: преобразование виртуальных адресов в физические, что может включать в себя пропадание TLB с дополнительным доступом к памяти, передачу запросов на шину памяти и т. д.) и контроллер памяти (запрос маршрутизации от контроллера ЦП к шине памяти контроллера, возможное ожидание для завершения цикла обновления банка памяти и т. д.). Подводя итог, можно сказать, что необработанный доступ к памяти имеет действительно большие затраты по сравнению с попаданием в кэш L1 или доступом к регистру. Разница в стоимости сопоставима с разницей в стоимости доступа к данным в памяти и во вторичном хранилище (HDD).

Кроме того, стоимость доступа к памяти будет расти с переходом от процессора к памяти. Доступ L2 обеспечит больший штраф, чем доступ к регистрам L1 или ЦП, доступ L3 обеспечит штраф больше, чем доступ L2, и, наконец, доступ к памяти предоставит штраф больше, чем доступ к памяти. Например, вы можете сравнить стоимость доступа к данным на разных уровнях иерархии памяти в следующей таблице (взято из http://www.anandtech.com/show/4955/the-bulldozer-review-amd-fx8150-tested/6)

Сравнение задержки кэша / памяти

-----------------------------------------------------------
|                                |L1| L2| L3| Main Memory |
-----------------------------------------------------------
|AMD FX-8150 (3.6GHz)            | 4| 21| 65| 195         |
-----------------------------------------------------------
|AMD Phenom II X4 975 BE (3.6GHz)| 3| 15| 59| 182         |
-----------------------------------------------------------
|AMD Phenom II X6 1100T (3.3GHz) | 3| 14| 55| 157         |
-----------------------------------------------------------
|Intel Core i5 2500K (3.3GHz)    | 4| 11| 25| 148         |
-----------------------------------------------------------

В отношении вашего конкретного случая:

0x1669  561 mov ecx, dword ptr [eax+0x8]        
0x166c  561 mov dword ptr [ebp-0x68], ecx   2.178s  1,614,000,000


0x1670  560 mov ecx, dword ptr [eax+0x8]    0.001s  4,000,000 /* confusing and looks like wrong report for me*/ 
0x1673  560 mov dword ptr [ebp-0x5c], ecx   1.193s  1,536,000,000

У вас есть штраф за разыменование значения Index в строке кода.

mov ecx, dword ptr [eax+0x8]

Обратите внимание, что это первый доступ к данным в каждом последующем узле вашего списка, до этого момента вы манипулируете только по адресу узла, но данные этого адреса и, следовательно, не имеют доступа к памяти. Вы заявили, что используете динамический список, и это плохо с точки зрения вероятности попадания в кеш. Кроме того, я предполагаю, что у вас достаточно большой список, что означает, что у вас будет кэш, заполненный данными, к которым ранее обращались (узлы списка, к которым обращались на предыдущих итерациях), и почти всегда будет иметь место пропадание кеша или попадание в кеш только в кэш L3 во время доступа к Index на каждой новой итерации. Но обратите внимание, что при первом доступе к Индексу при каждом потере кэша при каждой новой итерации данные, возвращаемые из памяти, будут храниться в кэше L1. И при повторном доступе к Index во время той же итерации цикла вы получите низкую стоимость попадания в кэш L1!

Поэтому я надеюсь, что предоставлю вам подробный ответ на оба ваших вопроса.

В отношении правильности сообщения VTune о правильности. Я хочу защищать разработчиков Intel VTune. Конечно, современные процессоры являются очень сложными устройствами с множеством улучшающих ILP технологий на плате, включая конвейерную обработку, суперскаларирование, выполнение вне порядка, прогнозирование ветвлений и т. Д., И, конечно, это делает подробный анализ производительности на уровне команд более трудным и более ценным. Но такие инструменты, как VTune, разработаны с учетом особенностей процессора, и я верю, что они не настолько глупы, чтобы разрабатывать и предоставлять инструмент или функцию, которые не имеют никакого смысла. Более того, похоже, что разработчики из Intel, как никто другой, не имеют доступа к полному пониманию всех особенностей процессора, и никто другой не может учесть эти детали при проектировании и разработке профилировщика.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...