Ассемблирование циклов Visual C++ 6.0

Тема в разделе "WASM.A&O", создана пользователем bers, 18 окт 2005.

  1. bers

    bers New Member

    Публикаций:
    0
    Регистрация:
    16 сен 2005
    Сообщения:
    139
    Адрес:
    Russia
    Тут исследовал зависимость скорости работы циклов в 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'а это кривовато.
     
  2. rgo

    rgo New Member

    Публикаций:
    0
    Регистрация:
    21 мар 2005
    Сообщения:
    87
    а ты говорил компилятору что компилировать надо с оптимизацией? Напрашивается же, использовать 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;

    }
     
  3. bers

    bers New Member

    Публикаций:
    0
    Регистрация:
    16 сен 2005
    Сообщения:
    139
    Адрес:
    Russia
    Да я же не столько о корявости ассемлирования говорил, сколько об ИНТЕРЕСНОСТИ ассемблирования одинакового куска

    кода чуть в разных условиях
     
  4. rgo

    rgo New Member

    Публикаций:
    0
    Регистрация:
    21 мар 2005
    Сообщения:
    87
    может быть интересность -- прямое следствие корявости?
     
  5. leo

    leo Active Member

    Публикаций:
    0
    Регистрация:
    4 авг 2004
    Сообщения:
    2.542
    Адрес:
    Russia
    bers

    Об "ИНТЕРЕСНОСТИ ассемблирования одинакового куска" ничего сказать не могу - и то и другое коряво.

    Но твои рассуждения о том, что "процессор не очень-то любит прыгать туда-сюда и естественно в 1-ом случае

    это выполнено лучше" - совершенно неверные. В современных процах рулит статическое и динамическое предсказание переходов, поэтому по JGE проц прыгает всего один раз из 100 - при выходе из цикла. Поэтому в обоих случаях предсказанный несовершаемый переход JGE ведет себя одинаково - как обычная ALU операция. Более того, теоретически два подряд идущих JCC - это хуже, чем разделенные другими операциями - эффективная латентность JCC <= 1 тика, но на самом деле есть еще скрытая латентность на коррекцию предсказания переходов (обычно не менее 2-х тиков), поэтому на некоторых процах, например в семействе P6, JCC не могут обрабатываться быстрее чем один за 2 тика. Но это так, к слову ;)

    А первый вариант получается быстрее второго по другой причине: во втором варианте практически все инструкции получаются зависимы по данным или по портам\исп.блокам, а в первом варианте вторая пара инструкций в начале цикла не зависит от первой и может выполняться параллельно.
     
  6. bers

    bers New Member

    Публикаций:
    0
    Регистрация:
    16 сен 2005
    Сообщения:
    139
    Адрес:
    Russia
    А по-моему и в первом и во втором случаях зависимость по "операндам\портам\исп.блокам" абсолютно одинакова. Различия этих кусков кода лишь в порядке следования JMPов. Ну и ECX c

    EDX местами поменяны. Так что по-моему ты не объяснил. Я ни

    в коем случае не настаиваю на своей точке зрения, наверняка она даже и неправильная, ну тогда объясните ПРАВИЛЬНО.
     
  7. Ustus

    Ustus New Member

    Публикаций:
    0
    Регистрация:
    8 авг 2005
    Сообщения:
    834
    Адрес:
    Харьков
    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



    Так что ИМХО - поток исполнения здесь вторичен, большую часть все равно жрут обращения к памяти.
     
  8. bers

    bers New Member

    Публикаций:
    0
    Регистрация:
    16 сен 2005
    Сообщения:
    139
    Адрес:
    Russia
    У меня P4 2400 c памятью DDR266, если интересно. Да, мои результаты приведены при компилировании без оптимизации - с оптимизацией более чем в 4 раза выигрыш
     
  9. leo

    leo Active Member

    Публикаций:
    0
    Регистрация:
    4 авг 2004
    Сообщения:
    2.542
    Адрес:
    Russia
    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 ;))
     
  10. Ustus

    Ustus New Member

    Публикаций:
    0
    Регистрация:
    8 авг 2005
    Сообщения:
    834
    Адрес:
    Харьков
    bers

    Ну, это легко догадаться, что без оптимизации

    А с оптимизацией это одинаковый код дает? А то мне интересно, а VC поблизости нет.

    Watcom 11.0c при максимальной оптимизации делает совершенно идентичный код:
    Код (Text):
    1.  
    2.     mov       edx,0x00004000
    3.     xor       eax,eax
    4. L$1:
    5.     add       eax,0x00000004
    6.     mov       -0x4[esp+eax],edx
    7.     cmp       eax,0x00000190
    8.     jne       L$1
    9.  


    а Borland 5.5 - нет:

    первый вариант:
    Код (Text):
    1.  
    2.     xor       eax,eax
    3. @3:
    4.     mov       edx,eax
    5.     inc       eax
    6.     mov       dword ptr [esp+4*edx],16384
    7.     cmp       eax,100
    8.     jl        short @3
    9.  


    второй вариант:
    Код (Text):
    1.  
    2.     xor       edx,edx
    3.     mov       eax,esp
    4. @10:
    5.     mov       dword ptr [eax],16384
    6.     inc       edx
    7.     add       eax,4
    8.     cmp       edx,100
    9.     jl        short @10
    10.  


    Вот и пойми их после этого... :)
     
  11. bers

    bers New Member

    Публикаций:
    0
    Регистрация:
    16 сен 2005
    Сообщения:
    139
    Адрес:
    Russia
    С оптимизацией в обоих случаях дает совершенно одинаковый код - заменяет такую корявую запись в память rep stos'ом, что дает 260 тактов.
     
  12. cresta

    cresta Active Member

    Публикаций:
    0
    Регистрация:
    13 июн 2004
    Сообщения:
    2.257
    Это с оптимизацией Ox. Оба цикла одинаковый код дают.


    Код (Text):
    1. 004010C0   . 57             PUSH EDI
    2. 004010C1   . B9 64000000    MOV ECX,64
    3. 004010C6   . B8 00400000    MOV EAX,4000
    4. 004010CB   . BF 08204000    MOV EDI,cr_v.00402008
    5. 004010D0   . F3:AB          REP STOS DWORD PTR ES:[EDI]
    6. 004010D2   . 5F             POP EDI
    7. 004010D3   . C3             RETN




    119 тиков на атлоне-2400.
     
  13. bers

    bers New Member

    Публикаций:
    0
    Регистрация:
    16 сен 2005
    Сообщения:
    139
    Адрес:
    Russia
    Мазово! Что-то сильно мало )) Выходит на Пне 4-ом больше чем в 2 раза
     
  14. bers

    bers New Member

    Публикаций:
    0
    Регистрация:
    16 сен 2005
    Сообщения:
    139
    Адрес:
    Russia
    Да, наверно немало значит выравнивание - вставлял кусок в другой код - показывал по 80 тактов при оптимизации. Но без оптимизации - те же числа.
     
  15. leo

    leo Active Member

    Публикаций:
    0
    Регистрация:
    4 авг 2004
    Сообщения:
    2.542
    Адрес:
    Russia
    bers

    80 тактов на 100 мувов - это ты загнул ;)

    У меня на P4 с rep stоsd лучше 200 тиков чего-то никак не выходит. А вот хуже (более 300) можно, если поиграть с выравниванием адреса edi
     
  16. bers

    bers New Member

    Публикаций:
    0
    Регистрация:
    16 сен 2005
    Сообщения:
    139
    Адрес:
    Russia
    Сам удивляюсь - не, реально пишет 80
     
  17. cresta

    cresta Active Member

    Публикаций:
    0
    Регистрация:
    13 июн 2004
    Сообщения:
    2.257
    Видимо так меряешь.

    Попробуй пример в аттаче, сколько будет показывать

    [​IMG] 163102909__cr_v.exe
     
  18. bers

    bers New Member

    Публикаций:
    0
    Регистрация:
    16 сен 2005
    Сообщения:
    139
    Адрес:
    Russia
    Показывает 364 в последних замерах. А какой код ты используешь для замера? Просто может быть в нем дело.

    Мой код:

    __asm

    {

    rdtsc

    mov [beg_end], eax

    }
     
  19. bers

    bers New Member

    Публикаций:
    0
    Регистрация:
    16 сен 2005
    Сообщения:
    139
    Адрес:
    Russia
    Понятно, ты же используешь и cpuid и xor eax, eax (хотя последний вклада в общее время приности мало, но все же) и еще EDX зачем то сохраняешь, хотя вполне можно обойтись и без ентого - отсюда и увеличение времени.
     
  20. cresta

    cresta Active Member

    Публикаций:
    0
    Регистрация:
    13 июн 2004
    Сообщения:
    2.257
    Что-то совсем примитивно меряешь :)

    Надо бы нормально вызывать rdtsc, кроме того, не мешало бы определит время, которое тратится на опрос самого счётчика ( обычно это ~105-106 тиков на моем проце), чтобы потом учесть это количество при калькуляции времени. Примерно так:








    Код (Text):
    1. //============== ===============================
    2.         int         dwStartL=0;     //младшие 32 бита счётчика
    3.         int         dwStartH=0;     //старшие 32 бита счётчика
    4.        
    5.         //для сохранения времени на опрос самого счётчика
    6.         int         counterL=0;
    7.         int         counterH=0;
    8.  
    9. //==================== инициализация счётчика ===========================
    10. void inline Init_Clock(){
    11.     __asm {
    12.         xor        eax,eax
    13.         cpuid
    14.         rdtsc
    15.         mov        dwStartL, eax        //пустой вызов счётчика для определения времени,
    16.         mov        dwStartH, edx        //необходимого на саму процедуру опроса
    17.         xor        eax,eax              //состояния счётчика
    18.         cpuid
    19.         xor        eax,eax              //второй вызов счётчика
    20.         cpuid
    21.         rdtsc
    22.         sub        eax,dwStartL         //;edx/eax - время, затрачиваемое на опрос счётчика
    23.         sbb        edx,dwStartH
    24.         mov        counterL,eax         //;<-сохранение для последующей коррекции показаний счётчика тиков
    25.         mov        counterH,edx
    26.  
    27.         xor        eax,eax              //собственно сам момент старта (третий вызов)
    28.         cpuid
    29.         rdtsc
    30.         mov        dwStartL, eax        //сохраняем время старта в глобальных переменных
    31.         mov        dwStartH, edx        //dwStartL/dwStartH
    32.         xor        eax,eax
    33.         cpuid
    34.         }
    35. }
    36.  
    37. //======================== останов счётчика =============================
    38. void inline    Stop_Clock(){
    39.     __asm{
    40.         xor        eax,eax                  
    41.         cpuid
    42.         rdtsc                           //eax/edx - состояние на момент окончания теста
    43.         sub        eax,dwStartL         //;--подсчет тиков на выполнение тестируемого кода
    44.         sbb        edx,dwStart
    45.         sub        eax,counterL         //;--вычитание времени на опрос счётчика тиков
    46.         sbb        edx,counterH
    47.         mov        dwStartL, eax        // dwStartL - младшие 32 бита количества тиков
    48.         mov        dwStartH, edx        // dwStartH - старшие 32 бита количества тиков
    49.         }
    50. }
    51.  
    52.  
    53. //===============================================================
    54. void    test_proc(){
    55.     char            caption[32];
    56.     char            buffer[1024];
    57.     char            temp[64];
    58.     const int       loops = 10;
    59.  
    60.     wsprintf (caption,"Тест из %lu проходов",loops);
    61.     buffer[0]=0;
    62.     HANDLE hThread = GetCurrentThread();
    63.     SetThreadPriority    (hThread, THREAD_PRIORITY_TIME_CRITICAL);
    64.     for (int n = loops; n --;){
    65.         //запуск счётчика(сохранение dwStartL/dwStartH)
    66.         Init_Clock();      
    67.                  
    68.         //здесь тестируемый код
    69.         for (int i=0; i<100; i++)
    70.             mas    [ i ] = 1<<14;
    71.  
    72.         //останов счётчика (в dwStartL/dwStartH - кол-во тиков)
    73.         Stop_Clock();                        
    74.  
    75.         wsprintf     (temp, "TICKS ~= %.10lu%.10lu \n", dwStartH, dwStartL);
    76.         strcat        (buffer, temp);
    77.         }
    78.    
    79.     SetThreadPriority    (hThread, THREAD_PRIORITY_NORMAL);
    80.     MessageBox    (NULL,buffer, caption,MB_OK);
    81.  
    82. }