Тут исследовал зависимость скорости работы циклов в Visual'е от количества итераций в теле и случайно наткнулся на первом этапе с одним интересным фактом. (Мерял RDTSC) Цикл вообще сначала имел следущий вид: for (i=0; i<100; _mas[ i++ ]=1<<14); (965 тактов) И тут я решил расписать его так: for (i=0; i<100; i++) _mas[ i ] = 1<<14; Получилось 1180 тактов. Мне это показалось странным, тк я всегда считал, что С ассемблирует эти куски кода абсолютно одинаково. Полез я в дизассемблер... Так было в первом случае: 0040106D | JMP SHORT main_1.0040108F 0040106F | MOV ECX,DWORD PTR SS:[EBP-194] 00401075 | MOV DWORD PTR SS:[EBP+ECX*4-190],4000 00401080 | MOV EDX,DWORD PTR SS:[EBP-194] 00401086 | ADD EDX,1 00401089 | MOV DWORD PTR SS:[EBP-194],EDX 0040108F | CMP DWORD PTR SS:[EBP-194],64 00401096 | JGE SHORT main_1.0040109A 00401098 | JMP SHORT main_1.0040106F 0040109A А так во втором: 0040106D | JMP SHORT main_1.0040107E 0040106F | MOV ECX,DWORD PTR SS:[EBP-194] 00401075 | ADD ECX,1 00401078 | MOV DWORD PTR SS:[EBP-194],ECX 0040107E | CMP DWORD PTR SS:[EBP-194],64 00401085 | JGE SHORT main_1.0040109A 00401087 | MOV EDX,DWORD PTR SS:[EBP-194] 0040108D | MOV DWORD PTR SS:[EBP+EDX*4-190],4000 00401098 | JMP SHORT main_1.0040106F 0040109A Как видно, код абсолютно одинаков, за исключением одной вещи : два JUMP'а - условный и безусловный - в 1-ом случае следуют друг за другом, во 2-ом же разбросаны по циклу. Как известно процессор не очень-то любит прыгать туда-сюда и естественно в 1-ом случае это выполнено лучше(фактически прыжок осуществляется всего один раз), чем во 2-ом, где 2 полноценных JMP'а. Не знаю как ассемблируют такое дело другие компилеры(Intel, Borland), но для Visual'а это кривовато.
а ты говорил компилятору что компилировать надо с оптимизацией? Напрашивается же, использовать ecx в качестве i, без лишних обращений к стеку. g++, с оптимизацией -O2, вот что делает (если я где-то синтаксис наврал извиняйте -- из AT&T переписывал): xor eax, eax 1: mov [ebp + 4*eax - 408], 16384 inc eax cmp eax, 99 jle 1b причём без разницы на каком варианте... или может всё от контекста зависит? я делал так: int main () { int _mas[100]; int i; for ... return 0; }
Да я же не столько о корявости ассемлирования говорил, сколько об ИНТЕРЕСНОСТИ ассемблирования одинакового куска кода чуть в разных условиях
bers Об "ИНТЕРЕСНОСТИ ассемблирования одинакового куска" ничего сказать не могу - и то и другое коряво. Но твои рассуждения о том, что "процессор не очень-то любит прыгать туда-сюда и естественно в 1-ом случае это выполнено лучше" - совершенно неверные. В современных процах рулит статическое и динамическое предсказание переходов, поэтому по JGE проц прыгает всего один раз из 100 - при выходе из цикла. Поэтому в обоих случаях предсказанный несовершаемый переход JGE ведет себя одинаково - как обычная ALU операция. Более того, теоретически два подряд идущих JCC - это хуже, чем разделенные другими операциями - эффективная латентность JCC <= 1 тика, но на самом деле есть еще скрытая латентность на коррекцию предсказания переходов (обычно не менее 2-х тиков), поэтому на некоторых процах, например в семействе P6, JCC не могут обрабатываться быстрее чем один за 2 тика. Но это так, к слову А первый вариант получается быстрее второго по другой причине: во втором варианте практически все инструкции получаются зависимы по данным или по портам\исп.блокам, а в первом варианте вторая пара инструкций в начале цикла не зависит от первой и может выполняться параллельно.
А по-моему и в первом и во втором случаях зависимость по "операндам\портам\исп.блокам" абсолютно одинакова. Различия этих кусков кода лишь в порядке следования JMPов. Ну и ECX c EDX местами поменяны. Так что по-моему ты не объяснил. Я ни в коем случае не настаиваю на своей точке зрения, наверняка она даже и неправильная, ну тогда объясните ПРАВИЛЬНО.
bers Какой у тебя проц? У меня получилось вот что (только массив и счетчик я слепил глобальные, а не в стеке и старался мерить на максимально прогретом кэше): PII Celeron (Mendocino): 1) 1236 2) 1260 P4 (Northwood): 1) 1544 2) 1512 Athlon (Thunderbird): Тут сложнее, большой разброс значений, но оба варианта примерно по 1100. Вообще у всех процов сильный разброс из-за зависимостям по памяти. Если же размер массива уменьшить до 32 (чтобы красивее в кэш влезал) то получим: PII Celeron (Mendocino): ~494 плюс-минус на обоих вариантах P4 (Northwood): 1) 800 (точно!) 2) 768 (точно!) Athlon (Thunderbird): все еще большой разброс, но минимальные значения такие: 1) 410 2) 396 Так что ИМХО - поток исполнения здесь вторичен, большую часть все равно жрут обращения к памяти.
У меня P4 2400 c памятью DDR266, если интересно. Да, мои результаты приведены при компилировании без оптимизации - с оптимизацией более чем в 4 раза выигрыш
bers > "зависимость по "операндам\портам\исп.блокам" абсолютно одинакова" Зависмимость конечно не одинаковая, но если все аккуратно расписать, то в обоих случаях должно получаться на P4 около 9 тиков на цикл или 900 c копейками на 100 проходов (копейки > 20 на выход по JGE). Но на деле тут вмешиваются какие-то тонкие хитрости. Возможно тебе просто повезло с выравниванием, т.к. у меня оба варианта при выравнивании первого JMP на 32 дают примерно одинаковое число тиков ~1180 (второй чуть похуже). Но интересно, что в первом варианте вставкой нопов перед перед первым JMP можно уменьшить задержку до тех-же 900 с копейками, а вот во втором вырианте это не помогает. С тем, что P4 в некоторых случаях может "ни с того ни с сего" скушать лишних 2 тика на цикл я и раньше сталкивался, но в чем причина не знаю - возможно все тот же "мистический и загадочный Т-кэш" PS: а код конечно ужасно корявый и при попытке частичной оптимизации можно запросто загнать P4 в реплэй. Например, если в первом варианте убрать первый JMP (т.к. он явно не нужен) и заменить cmp [..],64h на cmp edx,64h то задержка не уменьшается а возрастает аж до 1900 (!!!), но вставочкой нопов после cmp можно ее снизить до 860. Т.е. по-видимому тут еще рулят ограниченния на store-forwarding PPS: А чтобы отвязаться от задержек ОЗУ нужно просто прогонять тест в цикле несколько раз (не менее 4-х, лучше 8) и брать последние результаты - как учит великий Dr.Fog )
bers Ну, это легко догадаться, что без оптимизации А с оптимизацией это одинаковый код дает? А то мне интересно, а VC поблизости нет. Watcom 11.0c при максимальной оптимизации делает совершенно идентичный код: Код (Text): mov edx,0x00004000 xor eax,eax L$1: add eax,0x00000004 mov -0x4[esp+eax],edx cmp eax,0x00000190 jne L$1 а Borland 5.5 - нет: первый вариант: Код (Text): xor eax,eax @3: mov edx,eax inc eax mov dword ptr [esp+4*edx],16384 cmp eax,100 jl short @3 второй вариант: Код (Text): xor edx,edx mov eax,esp @10: mov dword ptr [eax],16384 inc edx add eax,4 cmp edx,100 jl short @10 Вот и пойми их после этого...
С оптимизацией в обоих случаях дает совершенно одинаковый код - заменяет такую корявую запись в память rep stos'ом, что дает 260 тактов.
Это с оптимизацией Ox. Оба цикла одинаковый код дают. Код (Text): 004010C0 . 57 PUSH EDI 004010C1 . B9 64000000 MOV ECX,64 004010C6 . B8 00400000 MOV EAX,4000 004010CB . BF 08204000 MOV EDI,cr_v.00402008 004010D0 . F3:AB REP STOS DWORD PTR ES:[EDI] 004010D2 . 5F POP EDI 004010D3 . C3 RETN 119 тиков на атлоне-2400.
Да, наверно немало значит выравнивание - вставлял кусок в другой код - показывал по 80 тактов при оптимизации. Но без оптимизации - те же числа.
bers 80 тактов на 100 мувов - это ты загнул У меня на P4 с rep stоsd лучше 200 тиков чего-то никак не выходит. А вот хуже (более 300) можно, если поиграть с выравниванием адреса edi
Показывает 364 в последних замерах. А какой код ты используешь для замера? Просто может быть в нем дело. Мой код: __asm { rdtsc mov [beg_end], eax }
Понятно, ты же используешь и cpuid и xor eax, eax (хотя последний вклада в общее время приности мало, но все же) и еще EDX зачем то сохраняешь, хотя вполне можно обойтись и без ентого - отсюда и увеличение времени.
Что-то совсем примитивно меряешь Надо бы нормально вызывать rdtsc, кроме того, не мешало бы определит время, которое тратится на опрос самого счётчика ( обычно это ~105-106 тиков на моем проце), чтобы потом учесть это количество при калькуляции времени. Примерно так: Код (Text): //============== =============================== int dwStartL=0; //младшие 32 бита счётчика int dwStartH=0; //старшие 32 бита счётчика //для сохранения времени на опрос самого счётчика int counterL=0; int counterH=0; //==================== инициализация счётчика =========================== void inline Init_Clock(){ __asm { xor eax,eax cpuid rdtsc mov dwStartL, eax //пустой вызов счётчика для определения времени, mov dwStartH, edx //необходимого на саму процедуру опроса xor eax,eax //состояния счётчика cpuid xor eax,eax //второй вызов счётчика cpuid rdtsc sub eax,dwStartL //;edx/eax - время, затрачиваемое на опрос счётчика sbb edx,dwStartH mov counterL,eax //;<-сохранение для последующей коррекции показаний счётчика тиков mov counterH,edx xor eax,eax //собственно сам момент старта (третий вызов) cpuid rdtsc mov dwStartL, eax //сохраняем время старта в глобальных переменных mov dwStartH, edx //dwStartL/dwStartH xor eax,eax cpuid } } //======================== останов счётчика ============================= void inline Stop_Clock(){ __asm{ xor eax,eax cpuid rdtsc //eax/edx - состояние на момент окончания теста sub eax,dwStartL //;--подсчет тиков на выполнение тестируемого кода sbb edx,dwStart sub eax,counterL //;--вычитание времени на опрос счётчика тиков sbb edx,counterH mov dwStartL, eax // dwStartL - младшие 32 бита количества тиков mov dwStartH, edx // dwStartH - старшие 32 бита количества тиков } } //=============================================================== void test_proc(){ char caption[32]; char buffer[1024]; char temp[64]; const int loops = 10; wsprintf (caption,"Тест из %lu проходов",loops); buffer[0]=0; HANDLE hThread = GetCurrentThread(); SetThreadPriority (hThread, THREAD_PRIORITY_TIME_CRITICAL); for (int n = loops; n --;){ //запуск счётчика(сохранение dwStartL/dwStartH) Init_Clock(); //здесь тестируемый код for (int i=0; i<100; i++) mas [ i ] = 1<<14; //останов счётчика (в dwStartL/dwStartH - кол-во тиков) Stop_Clock(); wsprintf (temp, "TICKS ~= %.10lu%.10lu \n", dwStartH, dwStartL); strcat (buffer, temp); } SetThreadPriority (hThread, THREAD_PRIORITY_NORMAL); MessageBox (NULL,buffer, caption,MB_OK); }