Цифровые раскопки: ассемблерные вставки. Часть 1
Цифровые раскопки: ассемблерные вставки. Часть 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}
Что тут кажется странным. Вообще-то довольно многое:
Кажется, есть ряд причин, по которым ассемблерные вставки просто не рекомендуется использовать. Впрочем по этой же ссылке приводятся и доводы в пользу использования ассемблерных вставок.
Доступ к локальным переменным функции 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 и сравнивает их результат.
Чтобы сделать код более переносимым имеет смысл передавать локальные переменные функции через регистры. По крайней мере так пропадают магически числа.