Цифровые раскопки: ассемблерные вставки. Часть 1 ================================================= .. post:: 29, June 2022 :tags: gps-sdr, asm :category: digital-excavation :author: engineer Как я уже писал ранее - мне пришлось в коде приёмника заменить функции, работающие с SIMD-инструкциями на их аналоги, написанные на чистом C. Пришло время покопаться в этих функциях и попытаться понять, почему они перестали работать. Сразу отмечу, что я ни разу не программист, поэтому не претендую на чистоту теории и практики. Код ассемблерных функций расположен в файлах sse.cpp и sse_new.cpp, но я исследовал только код из файла sse.cpp. Рассмотрим код первой же функции: .. code-block:: C :linenos: void sse_add(int16 *A, int16 *B, int32 cnt) { int32 cnt1; int32 cnt2; cnt1 = cnt / 8; cnt2 = (cnt - (8*cnt1)); if(((int)A%16) || ((int)B%16)) // unaligned version { __asm ( ".intel_syntax noprefix \n\t" //Set up for loop "mov edi, [ebp+8] \n\t" //Address of A "mov esi, [ebp+12] \n\t" //Address of B "mov ecx, [ebp-12] \n\t" //Counter 1 "jecxz Z%= \n\t" "L%=: \n\t" "movupd xmm0, [edi] \n\t" //Load from A "movupd xmm1, [esi] \n\t" //Load from B "paddw xmm0, xmm1 \n\t" //Multiply A*B "movupd [edi], xmm0 \n\t" //Move into A "add edi, 16 \n\t" "add esi, 16 \n\t" "loop L%= \n\t" //Loop if not done "Z%=: \n\t" "mov ecx, [ebp-16] \n\t" //Counter 2 "jecxz ZZ%= \n\t" "mov eax, 0 \n\t" "LL%=: \n\t" //Really finish off loop with non SIMD instructions "mov ax, [edi] \n\t" "add ax, [esi] \n\t" "mov [edi], ax \n\t" "add esi, 2 \n\t" "add edi, 2 \n\t" "loop LL%= \n\t" "ZZ%=: \n\t" "EMMS \n\t" ".att_syntax \n\t" : : "m" (A), "m" (B), "m" (cnt), "m" (cnt1), "m" (cnt2) : "%eax", "%ecx", "%edi", "%esi" ); } else { __asm ( ".intel_syntax noprefix \n\t" //Set up for loop "mov edi, [ebp+8] \n\t" //Address of A "mov esi, [ebp+12] \n\t" //Address of B "mov ecx, [ebp-12] \n\t" //Counter 1 "jecxz Z%= \n\t" "L%=: \n\t" "movapd xmm0, [edi] \n\t" //Load from A "paddw xmm0, [esi] \n\t" //Multiply A*B "movapd [edi], xmm0 \n\t" //Move into A "add edi, 16 \n\t" "add esi, 16 \n\t" "loop L%= \n\t" //Loop if not done "Z%=: \n\t" "mov ecx, [ebp-16] \n\t" //Counter 2 "jecxz ZZ%= \n\t" "mov eax, 0 \n\t" "LL%=: \n\t" //Really finish off loop with non SIMD instructions "mov ax, [edi] \n\t" "add ax, [esi] \n\t" "mov [edi], ax \n\t" "add esi, 2 \n\t" "add edi, 2 \n\t" "loop LL%= \n\t" "ZZ%=: \n\t" "EMMS \n\t" ".att_syntax \n\t" : : "m" (A), "m" (B), "m" (cnt), "m" (cnt1), "m" (cnt2) : "%eax", "%ecx", "%edi", "%esi" );//end __asm }//end if } Что тут кажется странным. Вообще-то довольно многое: 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 и сравнивает их результат. Чтобы сделать код более переносимым имеет смысл передавать локальные переменные функции через регистры. По крайней мере так пропадают магически числа.