Цифровые раскопки: ассемблерные вставки. Часть 1

Как я уже писал ранее - мне пришлось в коде приёмника заменить функции, работающие с SIMD-инструкциями на их аналоги, написанные на чистом C. Пришло время покопаться в этих функциях и попытаться понять, почему они перестали работать.

Сразу отмечу, что я ни разу не программист, поэтому не претендую на чистоту теории и практики.

Код ассемблерных функций расположен в файлах sse.cpp и sse_new.cpp, но я исследовал только код из файла sse.cpp. Рассмотрим код первой же функции:

 1void sse_add(int16 *A, int16 *B, int32 cnt)
 2{
 3
 4     int32 cnt1;
 5     int32 cnt2;
 6
 7     cnt1 = cnt / 8;
 8     cnt2 = (cnt - (8*cnt1));
 9
10     if(((int)A%16) || ((int)B%16)) // unaligned version
11     {
12
13             __asm
14             (
15                     ".intel_syntax noprefix         \n\t" //Set up for loop
16                     "mov edi, [ebp+8]                       \n\t" //Address of A
17                     "mov esi, [ebp+12]                      \n\t" //Address of B
18                     "mov ecx, [ebp-12]                      \n\t" //Counter 1
19                     "jecxz Z%=                                      \n\t"
20                     "L%=:                                           \n\t"
21                             "movupd xmm0,  [edi]    \n\t" //Load from A
22                             "movupd xmm1,  [esi]    \n\t" //Load from B
23                             "paddw  xmm0,  xmm1             \n\t" //Multiply A*B
24                             "movupd [edi], xmm0             \n\t" //Move into A
25                             "add    edi, 16                 \n\t"
26                             "add    esi, 16                 \n\t"
27                     "loop L%=                                       \n\t" //Loop if not done
28                     "Z%=:                                           \n\t"
29                     "mov ecx, [ebp-16]                      \n\t" //Counter 2
30                     "jecxz ZZ%=                                     \n\t"
31                     "mov eax, 0                                     \n\t"
32                     "LL%=:                                          \n\t" //Really finish off loop with non SIMD instructions
33                             "mov ax, [edi]                  \n\t"
34                             "add ax, [esi]                  \n\t"
35                             "mov [edi], ax                  \n\t"
36                             "add esi, 2                             \n\t"
37                             "add edi, 2                             \n\t"
38                     "loop LL%=                                      \n\t"
39                     "ZZ%=:                                          \n\t"
40                     "EMMS                                           \n\t"
41                     ".att_syntax                            \n\t"
42                     :
43                     : "m" (A), "m" (B), "m" (cnt), "m" (cnt1), "m" (cnt2)
44                     : "%eax", "%ecx", "%edi", "%esi"
45             );
46     }
47     else
48     {
49             __asm
50             (
51                     ".intel_syntax noprefix         \n\t" //Set up for loop
52                     "mov edi, [ebp+8]                       \n\t" //Address of A
53                     "mov esi, [ebp+12]                      \n\t" //Address of B
54                     "mov ecx, [ebp-12]                      \n\t" //Counter 1
55                     "jecxz Z%=                                      \n\t"
56                     "L%=:                                           \n\t"
57                             "movapd xmm0,  [edi]    \n\t" //Load from A
58                             "paddw  xmm0,  [esi]    \n\t" //Multiply A*B
59                             "movapd [edi], xmm0             \n\t" //Move into A
60                             "add    edi, 16                 \n\t"
61                             "add    esi, 16                 \n\t"
62                     "loop L%=                                       \n\t" //Loop if not done
63                     "Z%=:                                           \n\t"
64                     "mov ecx, [ebp-16]                      \n\t" //Counter 2
65                     "jecxz ZZ%=                                     \n\t"
66                     "mov eax, 0                                     \n\t"
67                     "LL%=:                                          \n\t" //Really finish off loop with non SIMD instructions
68                             "mov ax, [edi]                  \n\t"
69                             "add ax, [esi]                  \n\t"
70                             "mov [edi], ax                  \n\t"
71                             "add esi, 2                             \n\t"
72                             "add edi, 2                             \n\t"
73                     "loop LL%=                                      \n\t"
74                     "ZZ%=:                                          \n\t"
75                     "EMMS                                           \n\t"
76                     ".att_syntax                            \n\t"
77                     :
78                     : "m" (A), "m" (B), "m" (cnt), "m" (cnt1), "m" (cnt2)
79                     : "%eax", "%ecx", "%edi", "%esi"
80             );//end __asm
81     }//end if
82}

Что тут кажется странным. Вообще-то довольно многое:

  1. Кажется, есть ряд причин, по которым ассемблерные вставки просто не рекомендуется использовать. Впрочем по этой же ссылке приводятся и доводы в пользу использования ассемблерных вставок.

  2. Доступ к локальным переменным функции cnt1 и cnt2 осуществляется как к памяти. С одной стороны - это выглядит логичным, ведь, кажется, под локальные переменные действительно выделяется память в стеке функции. Но вот доступ к памяти, хранящей эти переменные нетривиален. Как будто разумнее и гораздо удобнее было бы обращаться к ним как к регистрам. А так фактически используется криптокод вида «mov ecx, [ebp-12]» и «mov ecx, [ebp-16]». Вот эти магические числа (-12 и -16) как будто бы и изменились в той версии g++, которую я использую.

По-хорошему, конечно, надо почитать подробно про соглашения на вызов функций актуальные для C++/x86, например тут, и разобраться как и на что выделяется память в функции (как и по каким адреса располагаются локальные переменные и прочее). Но пока не хочется погружаться в такие глубины. По-быстрому можно в отладчике посмотреть адреса локальных переменных и значение ebp-регистра. И правильную разность поместить в строки 18 и 29 (благо ассемблерный код содержит комментарии, позволяющие понять что к чему).

Вообще в перспективе есть желание сравнить вот такой ручной код с кодом, генерируемым g++ при включении опций использования SIMD-инструкций. Или переписать код с использованием интринсиков. Но это потом, а пока пусть сделаю по-быстрому… Результат исправлений доступен по ссылке. Для проверки работы SIMD-версий функций используется тестовая программа simd-test.cpp. Она заполняет тестовые вектора данными и вызывает две версии функции x86 и SIMD и сравнивает их результат.

Чтобы сделать код более переносимым имеет смысл передавать локальные переменные функции через регистры. По крайней мере так пропадают магически числа.