C против ассемблера против производительности NEON

Я работаю над приложением для iPhone, которое выполняет обработку изображений в реальном времени. Одним из первых шагов в его конвейере является преобразование изображения BGRA в оттенки серого. Я попробовал несколько разных методов, и разница в результатах по времени оказалась намного больше, чем я предполагал. Сначала я попытался использовать C. Я аппроксимирую преобразование в яркость, добавляя B+2*G+R/4.

void BGRA_To_Byte(Image<BGRA> &imBGRA, Image<byte> &imByte)
{
uchar *pIn = (uchar*) imBGRA.data;
uchar *pLimit = pIn + imBGRA.MemSize();

uchar *pOut = imByte.data;
for(; pIn < pLimit; pIn+=16)   // Does four pixels at a time
{
    unsigned int sumA = pIn[0] + 2 * pIn[1] + pIn[2];
    pOut[0] = sumA / 4;
    unsigned int sumB = pIn[4] + 2 * pIn[5] + pIn[6];
    pOut[1] = sumB / 4;
    unsigned int sumC = pIn[8] + 2 * pIn[9] + pIn[10];
    pOut[2] = sumC / 4;
    unsigned int sumD = pIn[12] + 2 * pIn[13] + pIn[14];
    pOut[3] = sumD / 4;
    pOut +=4;
}       
}

Этот код занимает 55 мс для преобразования изображения 352x288. Затем я нашел код на ассемблере, который делает то же самое.

void BGRA_To_Byte(Image<BGRA> &imBGRA, Image<byte> &imByte)
{
uchar *pIn = (uchar*) imBGRA.data;
uchar *pLimit = pIn + imBGRA.MemSize();

unsigned int *pOut = (unsigned int*) imByte.data;

for(; pIn < pLimit; pIn+=16)   // Does four pixels at a time
{
  register unsigned int nBGRA1 asm("r4");
  register unsigned int nBGRA2 asm("r5");
  unsigned int nZero=0;
  unsigned int nSum1;
  unsigned int nSum2;
  unsigned int nPacked1;
  asm volatile(
           
               "ldrd %[nBGRA1], %[nBGRA2], [ %[pIn], #0]       \n"   // Load in two BGRA words
               "usad8 %[nSum1], %[nBGRA1], %[nZero]  \n"  // Add R+G+B+A 
               "usad8 %[nSum2], %[nBGRA2], %[nZero]  \n"  // Add R+G+B+A 
               "uxtab %[nSum1], %[nSum1], %[nBGRA1], ROR #8    \n"   // Add G again
               "uxtab %[nSum2], %[nSum2], %[nBGRA2], ROR #8    \n"   // Add G again
               "mov %[nPacked1], %[nSum1], LSR #2 \n"    // Init packed word   
               "mov %[nSum2], %[nSum2], LSR #2 \n"   // Div by four
               "add %[nPacked1], %[nPacked1], %[nSum2], LSL #8 \n"   // Add to packed word                 

               "ldrd %[nBGRA1], %[nBGRA2], [ %[pIn], #8]       \n"   // Load in two more BGRA words
               "usad8 %[nSum1], %[nBGRA1], %[nZero]  \n"  // Add R+G+B+A 
               "usad8 %[nSum2], %[nBGRA2], %[nZero]  \n"  // Add R+G+B+A 
               "uxtab %[nSum1], %[nSum1], %[nBGRA1], ROR #8    \n"   // Add G again
               "uxtab %[nSum2], %[nSum2], %[nBGRA2], ROR #8    \n"   // Add G again
               "mov %[nSum1], %[nSum1], LSR #2 \n"   // Div by four
               "add %[nPacked1], %[nPacked1], %[nSum1], LSL #16 \n"   // Add to packed word
               "mov %[nSum2], %[nSum2], LSR #2 \n"   // Div by four
               "add %[nPacked1], %[nPacked1], %[nSum2], LSL #24 \n"   // Add to packed word                 
              
               ///////////
               ////////////
               
               : [pIn]"+r" (pIn), 
         [nBGRA1]"+r"(nBGRA1),
         [nBGRA2]"+r"(nBGRA2),
         [nZero]"+r"(nZero),
         [nSum1]"+r"(nSum1),
         [nSum2]"+r"(nSum2),
         [nPacked1]"+r"(nPacked1)
               :
               : "cc"  );
  *pOut = nPacked1;
  pOut++;
 }
 }

Эта функция преобразует одно и то же изображение за 12 мс, почти в 5 раз быстрее! Раньше я не программировал на ассемблере, но предполагал, что он будет не намного быстрее, чем C для такой простой операции. Вдохновленный этим успехом, я продолжил поиск и нашел пример преобразования NEON здесь.

void greyScaleNEON(uchar* output_data, uchar* input_data, int tot_pixels)
{
__asm__ volatile("lsr          %2, %2, #3      \n"
                 "# build the three constants: \n"
                 "mov         r4, #28          \n" // Blue channel multiplier
                 "mov         r5, #151         \n" // Green channel multiplier
                 "mov         r6, #77          \n" // Red channel multiplier
                 "vdup.8      d4, r4           \n"
                 "vdup.8      d5, r5           \n"
                 "vdup.8      d6, r6           \n"
                 "0:                           \n"
                 "# load 8 pixels:             \n"
                 "vld4.8      {d0-d3}, [%1]!   \n"
                 "# do the weight average:     \n"
                 "vmull.u8    q7, d0, d4       \n"
                 "vmlal.u8    q7, d1, d5       \n"
                 "vmlal.u8    q7, d2, d6       \n"
                 "# shift and store:           \n"
                 "vshrn.u16   d7, q7, #8       \n" // Divide q3 by 256 and store in the d7
                 "vst1.8      {d7}, [%0]!      \n"
                 "subs        %2, %2, #1       \n" // Decrement iteration count
                 "bne         0b            \n" // Repeat unil iteration count is not zero
                 :
                 :  "r"(output_data),           
                 "r"(input_data),           
                 "r"(tot_pixels)        
                 : "r4", "r5", "r6"
                 );
}

В временные результаты было трудно поверить. Он преобразует одно и то же изображение за 1 мс. В 12 раз быстрее, чем ассемблер, и в поразительные 55 раз быстрее, чем C. Я понятия не имел, что такой прирост производительности возможен. В связи с этим у меня есть несколько вопросов. Во-первых, я делаю что-то ужасно неправильно в коде C? Мне до сих пор трудно поверить, что это так медленно. Во-вторых, если эти результаты вообще точны, в каких ситуациях я могу ожидать этих успехов? Вы, наверное, можете себе представить, как я взволнован перспективой заставить другие части моего конвейера работать в 55 раз быстрее. Должен ли я изучать ассемблер/NEON и использовать их внутри любого цикла, который занимает значительное количество времени?

Обновление 1: я разместил вывод ассемблера из моей функции C в текстовом файле по адресу http://temp-share.com/show/f3Yg87jQn Он был слишком большим, чтобы включать его прямо сюда.

Время выполняется с использованием функций OpenCV.

double duration = static_cast<double>(cv::getTickCount()); 
//function call 
duration = static_cast<double>(cv::getTickCount())-duration;
duration /= cv::getTickFrequency();
//duration should now be elapsed time in ms

Полученные результаты

Я протестировал несколько предложенных улучшений. Во-первых, по рекомендации Виктора я переупорядочил внутренний цикл так, чтобы все выборки ставились на первое место. Внутренний цикл тогда выглядел так.

for(; pIn < pLimit; pIn+=16)   // Does four pixels at a time
{     
  //Jul 16, 2012 MR: Read and writes collected
  sumA = pIn[0] + 2 * pIn[1] + pIn[2];
  sumB = pIn[4] + 2 * pIn[5] + pIn[6];
  sumC = pIn[8] + 2 * pIn[9] + pIn[10];
  sumD = pIn[12] + 2 * pIn[13] + pIn[14];
  pOut +=4;
  pOut[0] = sumA / 4;
  pOut[1] = sumB / 4;
  pOut[2] = sumC / 4;
  pOut[3] = sumD / 4;
}

Это изменение сократило время обработки до 53 мс, что на 2 мс больше. Затем, как рекомендовал Виктор, я изменил свою функцию на получение как uint. Тогда внутренний цикл выглядел как

unsigned int* in_int = (unsigned int*) original.data;
unsigned int* end = (unsigned int*) in_int + out_length;
uchar* out = temp.data;

for(; in_int < end; in_int+=4)   // Does four pixels at a time
{
    unsigned int pixelA = in_int[0];
    unsigned int pixelB = in_int[1];
    unsigned int pixelC = in_int[2];
    unsigned int pixelD = in_int[3];
        
    uchar* byteA = (uchar*)&pixelA;
    uchar* byteB = (uchar*)&pixelB;
    uchar* byteC = (uchar*)&pixelC;
    uchar* byteD = (uchar*)&pixelD;         
        
    unsigned int sumA = byteA[0] + 2 * byteA[1] + byteA[2];
    unsigned int sumB = byteB[0] + 2 * byteB[1] + byteB[2];
    unsigned int sumC = byteC[0] + 2 * byteC[1] + byteC[2];
    unsigned int sumD = byteD[0] + 2 * byteD[1] + byteD[2];

    out[0] = sumA / 4;
    out[1] = sumB / 4;
    out[2] = sumC / 4;
    out[3] = sumD / 4;
    out +=4;
    }

Эта модификация имела драматический эффект, снизив время обработки до 14 мс, падение на 39 мс (75%). Этот последний результат очень близок к производительности ассемблера в 11 мс. Последняя оптимизация, рекомендованная Робом, заключалась в включении ключевого слова __restrict. Я добавил его перед каждым объявлением указателя, изменив следующие строки

__restrict unsigned int* in_int = (unsigned int*) original.data;
unsigned int* end = (unsigned int*) in_int + out_length;
__restrict uchar* out = temp.data;  
...
__restrict uchar* byteA = (uchar*)&pixelA;
__restrict uchar* byteB = (uchar*)&pixelB;
__restrict uchar* byteC = (uchar*)&pixelC;
__restrict uchar* byteD = (uchar*)&pixelD;  
...     

Эти изменения не оказали заметного влияния на время обработки. Спасибо за вашу помощь, в будущем я буду уделять гораздо больше внимания управлению памятью.


person Hammer    schedule 16.07.2012    source источник
comment
Не могли бы вы выложить сборку, которую сделал компилятор?   -  person harold    schedule 16.07.2012
comment
Эээ... пожалуйста, поправьте меня, если я ошибаюсь, но NEON работает на отдельном аппаратном массиве DSP? Если это так, может быть, это объясняет, почему это немного быстрее?   -  person Martin James    schedule 16.07.2012
comment
То же железо, тот же чип. Это расширение набора инструкций, такое же, как SSE или MMX.   -  person Nils Pipenbrinck    schedule 16.07.2012
comment
@harold Я пошел за ассемблером, созданным компилятором, но он абсурдно велик. Я в xcode 4.3.2 и пошел смотреть сборку в помощнике редактора. Моя функция в C имеет длину 23 строки без включений, но сгенерированный ассемблер имеет длину 9029 строк. Любая идея, почему это было бы? Я позволяю компилятору максимизировать оптимизацию кода C.   -  person Hammer    schedule 16.07.2012
comment
Это действительно часть одной и той же функции?   -  person harold    schedule 16.07.2012
comment
Извините, я совершенно новичок в ассемблере. Я предположил, что если я создам новый файл .cpp, который содержит только функцию и не включает, сгенерированная сборка будет отражать только содержимое функции. Это хорошее предположение? Есть ли лучший способ изолировать соответствующий ассемблер?   -  person Hammer    schedule 16.07.2012
comment
@Hammer: Можете ли вы рассказать нам, как вы сделали эти измерения? Спасибо   -  person A_nto2    schedule 16.07.2012
comment
@harold, я включил ссылку на вывод ассемблера в вопрос   -  person Hammer    schedule 16.07.2012
comment
@ A_nto2 Я использую openCV для большей части этого проекта. У них есть вызовы функций, которые полезны для определения времени. Последовательность времени функции обычно идет. продолжительность = static_cast‹double›(cv::getTickCount()); длительность вызова функции = static_cast‹double›(cv::getTickCount())-duration; продолжительность /= cv::getTickFrequency(); // прошедшее время в мс   -  person Hammer    schedule 16.07.2012
comment
Хорошо, я посмотрел этот файл, код там, примерно с 72 по 159 строку. Остальное просто .. мелочи.   -  person harold    schedule 16.07.2012
comment
Если вам действительно нужна быстрая обработка видео, могу ли я предложить взглянуть на фрагментные шейдеры OpenGL ES 2.0? Графический процессор даже быстрее справляется с простыми параллельными операциями, чем подпрограммы, оптимизированные для NEON. Я написал фреймворк с открытым исходным кодом для этого: github.com/BradLarson/GPUImage и я видел более 100 раз ускорение обработки изображений на графических процессорах iOS по сравнению с функциями C, привязанными к процессору. Я воспроизвел некоторые функциональные возможности OpenCV, и моя цель — сделать из них как можно больше.   -  person Brad Larson    schedule 16.07.2012
comment
@BradLarson Насколько мне известно, нет хорошего способа вернуть информацию из графического процессора после ее поступления. Это правда? Преобразование в оттенки серого в этом случае является лишь первым шагом в более крупном конвейере обработки. Я бы хотел использовать GPU, но, насколько я понимаю, он полезен только в качестве последнего шага в конвейере.   -  person Hammer    schedule 16.07.2012
comment
@Hammer - Нет, вы вполне способны извлекать данные после обработки. В приведенной выше структуре у меня есть класс для предоставления вам необработанных байтов для вашего обработанного изображения в формате RGBA или BGRA (немного быстрее). Всю эту обработку можно выполнять вне экрана, поэтому вы можете заменить медленные части вашего конвейера элементами, привязанными к графическому процессору. При переходе к графическому процессору и обратно возникают некоторые накладные расходы, но для необработанных байтов они, как правило, меньше, чем экономия за счет вычислений на изображениях размером более 320x240 или около того.   -  person Brad Larson    schedule 16.07.2012
comment
Что касается нового раздела «Результаты»: похоже, большая победа может быть связана с более эффективной загрузкой данных. Если вы можете загрузить 128-битное слово данных вместо 4x32-битных слов, вы можете получить дополнительный прирост производительности; посмотрите в руководстве по внутренним функциям ARM NEON типы данных: .pdf   -  person comingstorm    schedule 17.07.2012
comment
@comingstorm, как я могу загрузить 128-битное слово сразу? Просто сделайте char[] и memcpy данные? Является ли memcpy таким же быстрым, как присваивание? Я не думаю, что есть какие-либо 128-битные базовые типы, которые я мог бы использовать, как я использовал uint выше. Спасибо   -  person Hammer    schedule 17.07.2012
comment
Просто чтобы уточнить, я имею в виду, как я могу сделать это на C, это кажется потенциальным улучшением.   -  person Hammer    schedule 17.07.2012
comment
В официальном документе, указанном в моем комментарии (в котором объясняются внутренние функции ARM NEON), упоминается включаемый файл "arm_neon.h", который определяет тип uint8x16_t, который, вероятно, вам нужен.   -  person comingstorm    schedule 17.07.2012


Ответы (4)


Здесь есть объяснение некоторых причин «успеха» NEON: http://hilbert-space.de/?p= 22

Попробуйте скомпилировать код C с ключами "-S -O3", чтобы увидеть оптимизированный вывод компилятора GCC.

ИМХО, ключом к успеху является оптимизированный шаблон чтения/записи, используемый обеими версиями сборки. А NEON/MMX/другие векторные движки также поддерживают насыщение (зажим результатов до 0..255 без использования «целых без знака»).

Смотрите эти строки в цикле:

unsigned int sumA = pIn[0] + 2 * pIn[1] + pIn[2];
pOut[0] = sumA / 4;
unsigned int sumB = pIn[4] + 2 * pIn[5] + pIn[6];
pOut[1] = sumB / 4;
unsigned int sumC = pIn[8] + 2 * pIn[9] + pIn[10];
pOut[2] = sumC / 4;
unsigned int sumD = pIn[12] + 2 * pIn[13] + pIn[14];
pOut[3] = sumD / 4;
pOut +=4;

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

// and the pIn reads can be combined into a single 4-byte fetch
sumA = pIn[0] + 2 * pIn[1] + pIn[2];
sumB = pIn[4] + 2 * pIn[5] + pIn[6];
sumC = pIn[8] + 2 * pIn[9] + pIn[10];
sumD = pIn[12] + 2 * pIn[13] + pIn[14];
pOut +=4;
pOut[0] = sumA / 4;
pOut[1] = sumB / 4;
pOut[2] = sumC / 4;
pOut[3] = sumD / 4;

Имейте в виду, что строка «unsigned in sumA» здесь может действительно означать вызов alloca() (распределение в стеке), поэтому вы тратите много циклов на временные распределения var (вызов функции 4 раза).

Кроме того, индексация pIn[i] выполняет только однобайтовую выборку из памяти. Лучший способ сделать это — прочитать int, а затем извлечь отдельные байты. Чтобы ускорить процесс, используйте "unsgined int*" для чтения 4 байтов (pIn[i * 4 + 0], pIn[i * 4 + 1], pIn[i * 4 + 2], pIn[i * 4 + 3]).

Версия NEON явно лучше: линии

             "# load 8 pixels:             \n"
             "vld4.8      {d0-d3}, [%1]!   \n"

и

             "#save everything in one shot   \n"
             "vst1.8      {d7}, [%0]!      \n"

сэкономить большую часть времени для доступа к памяти.

person Viktor Latypov    schedule 16.07.2012
comment
другими словами - плохой код не может быть быстрым - если вы хотите писать быстрые программы на C, вам действительно нужно знать базовый ассемблер, иначе вы облажались... - person rezna; 16.07.2012
comment
C - кроссплатформенный ассемблер :) . Я не думаю, что сборка абсолютно необходима, но знание того, как все работает и где могут быть узкие места, безусловно, очень помогает. И здесь доступ к памяти почти всегда медленнее, чем арифметика. - person Viktor Latypov; 16.07.2012
comment
Кроме того, следует подчеркнуть, что операции NEON работают с векторами, поэтому вы часто получаете значительное ускорение только за счет их SIMD-природы и выделенного оборудования, которое процессоры ARMv6 и ARMv7 имеют для выполнения этих параллельных операций. Я регулярно видел отчеты о 3-4-кратном ускорении реализации NEON чего-то, что было сделано раньше в стандартной сборке ARM с ручной настройкой. - person Brad Larson; 16.07.2012
comment
@BradLarson: Конечно, моя немного лучшая версия далека от оптимальной, но последнее умножение-сложение (четыре строки), очевидно, можно выполнить в одной (или нескольких) инструкции, включающей только регистры с приличной архитектурой SIMD. - person Viktor Latypov; 16.07.2012
comment
@ViktorLatypov спасибо, ваши предложения значительно улучшили производительность моего кода C. Я буду применять эти принципы к другим местам в своем пайплайне, а затем изучу, какие из них все еще могут выиграть от использования NEON. Очевидно, мне еще многое предстоит узнать об управлении памятью и ее эффективности. - person Hammer; 16.07.2012
comment
@Hammer: Спасибо за ответ. На самом деле, я провел прошлую неделю, профилируя код трехмерной объемной обработки, и принцип памяти является узким местом прямо сейчас слишком глубоко у меня в голове :) После пары, казалось бы, очевидных оптимизаций, производительность увеличилась почти в 100 раз. . - person Viktor Latypov; 16.07.2012
comment
@ViktorLatypov может sumA = pIn[0] + 2 * pIn[1] + pIn[2]; быть быстрее, если я использую сдвиг битов вместо умножения или использую FMA для выполнения части 2 * pIn[1] + pIn[2]? - person greenfox; 28.12.2014

Если производительность критически важна (как это обычно бывает при обработке изображений в реальном времени), вам нужно обратить внимание на машинный код. Как вы уже поняли, использование векторных инструкций (которые предназначены для таких вещей, как обработка изображений в реальном времени) может быть особенно важно, и компиляторам сложно автоматически эффективно использовать векторные инструкции.

Прежде чем приступить к сборке, вам следует попробовать использовать внутренние функции компилятора. Встроенные функции компилятора не более переносимы, чем ассемблер, но их легче читать и писать, а компилятору легче с ними работать. Помимо проблем с ремонтопригодностью, проблема производительности ассемблера заключается в том, что он фактически отключает оптимизатор (вы использовали соответствующий флаг компилятора, чтобы включить его, верно?). То есть: со встроенным ассемблером компилятор не может настроить назначение регистров и т. д., поэтому, если вы не пишете весь свой внутренний цикл на ассемблере, он все равно может быть не таким эффективным, как мог бы быть.

Тем не менее, вы по-прежнему сможете с пользой использовать свои новообретенные знания в области сборки, поскольку теперь вы можете проверять сборку, созданную вашим компилятором, и выяснять, не является ли она глупой. Если это так, вы можете настроить код C (возможно, выполнив некоторую конвейерную обработку вручную, если компилятор не не удается), перекомпилируйте его, посмотрите на вывод сборки, чтобы увидеть, делает ли теперь компилятор то, что вы хотите, а затем протестируйте, чтобы увидеть, действительно ли он работает быстрее...

Если вы пробовали вышеописанное и все еще не можете спровоцировать компилятор на правильные действия, продолжайте и напишите свой внутренний цикл на ассемблере (и, опять же, проверьте, действительно ли результат получается быстрее). По причинам, описанным выше, обязательно получите полный внутренний цикл, включая ветвь цикла.

Наконец, как упоминали другие, найдите время, чтобы попытаться выяснить, что такое «правильная вещь». Еще одним преимуществом изучения архитектуры вашей машины является то, что она дает вам мысленную модель того, как все работает, поэтому у вас будет больше шансов понять, как составить эффективный код.

person comingstorm    schedule 16.07.2012
comment
Спасибо, я буду использовать вашу процедуру, когда буду изучать остальную часть своего конвейера в поисках возможных оптимизаций. - person Hammer; 16.07.2012

В ответе Виктора Латыпова много полезной информации, но я хочу указать еще на одну вещь: в вашей исходной функции C компилятор не может сказать, что pIn и pOut указывают на непересекающиеся области памяти. Теперь посмотрите на эти строки:

pOut[0] = sumA / 4;
unsigned int sumB = pIn[4] + 2 * pIn[5] + pIn[6];

Компилятор должен предположить, что pOut[0] может быть таким же, как pIn[4] или pIn[5] или pIn[6] (или любое другое pIn[x]). Таким образом, он в принципе не может изменить порядок любого кода в вашем цикле.

Вы можете сообщить компилятору, что pIn и pOut не перекрываются, объявив их __restrict:

__restrict uchar *pIn = (uchar*) imBGRA.data;
__restrict uchar *pOut = imByte.data;

Это может немного ускорить вашу исходную версию C.

person rob mayoff    schedule 16.07.2012
comment
Как объяснено в обновлении вопроса, я попытался добавить ключевое слово ограничения, но это не дало большого эффекта. Правильно ли я его использовал? Стоит ли всегда использовать его, если я знаю, что мои указатели не перекрываются? - person Hammer; 16.07.2012
comment
Смысл добавления restrict заключался в том, чтобы позволить компилятору изменить порядок загрузки и сохранения. Вы уже сделали это вручную, поэтому добавление restrict не позволило компилятору сделать что-то новое. Я использую restrict только тогда, когда профилировщик говорит мне, что мне нужно ускорить выполнение функции. Я не просто посыпаю им свой код. - person rob mayoff; 16.07.2012

Это своего рода подбрасывание между производительностью и ремонтопригодностью. Как правило, быстрая загрузка приложения и его функционирование очень приятны для пользователя, но есть и компромисс. Теперь ваше приложение довольно сложно поддерживать, и прирост скорости может быть необоснованным. Если пользователи вашего приложения жаловались на то, что оно работает медленно, то эти оптимизации стоят затраченных усилий и отсутствия ремонтопригодности, но если это связано с вашей потребностью ускорить ваше приложение, вам не следует заходить так далеко в оптимизацию. Если вы выполняете эти преобразования изображений при запуске приложения, то скорость не имеет значения, но если вы постоянно делаете их (и делаете много) во время работы приложения, тогда они имеют больше смысла. Оптимизируйте только те части приложения, где пользователь проводит время и действительно испытывает замедление.

Также, глядя на сборку, они не используют деление, а только умножение, поэтому посмотрите на это для вашего кода C. Другой пример заключается в том, что он оптимизирует ваше умножение на 2 из двух сложений. Это снова может быть еще одним трюком, поскольку умножение может быть медленнее в приложении iPhone, чем сложение.

person sean    schedule 16.07.2012
comment
Если компилятор не оптимизирует эти тривиальные деления, то он идиот, и авторам должно быть не по себе. - person harold; 16.07.2012
comment
Это справедливое замечание, опять же, у нас нет ассемблерного вывода C-кода OP, так что это просто мое предположение. - person sean; 16.07.2012
comment
Возможно, также могут возникнуть проблемы с отладкой и выпуском сборок. Компилятор не будет изменять asm volatile во время сеанса отладки, но это вполне может повлиять на код C. - person cli_hlt; 16.07.2012
comment
Я позаботился о том, чтобы скомпилировать режим выпуска для каждого теста. Кроме того, приложение отслеживает текстуры в реальном времени, и чем быстрее оно работает, тем надежнее отслеживание, поэтому оптимизация имеет первостепенное значение. - person Hammer; 16.07.2012
comment
Аккуратный пост; вопрос: замечает ли заказчик дельту в 54 мс? Если у вас много фото, то, думаю, да. С другой стороны, код C легче читать. С другой стороны, код C на самом деле не так уж легко читать. - person Yusuf X; 16.07.2012
comment
+1 только за «С другой стороны, код C легче читать. С другой стороны, код C на самом деле не так уж и легче читать, LOL! - person Martin James; 16.07.2012