Является ли встроенный язык ассемблера медленнее, чем собственный код C++?

Я попытался сравнить производительность встроенного языка ассемблера и кода C++, поэтому я написал функцию, которая добавляет два массива размером 2000 для 100000 раз. Вот код:

#define TIMES 100000
void calcuC(int *x,int *y,int length)
{
    for(int i = 0; i < TIMES; i++)
    {
        for(int j = 0; j < length; j++)
            x[j] += y[j];
    }
}


void calcuAsm(int *x,int *y,int lengthOfArray)
{
    __asm
    {
        mov edi,TIMES
        start:
        mov esi,0
        mov ecx,lengthOfArray
        label:
        mov edx,x
        push edx
        mov eax,DWORD PTR [edx + esi*4]
        mov edx,y
        mov ebx,DWORD PTR [edx + esi*4]
        add eax,ebx
        pop edx
        mov [edx + esi*4],eax
        inc esi
        loop label
        dec edi
        cmp edi,0
        jnz start
    };
}

Вот main():

int main() {
    bool errorOccured = false;
    setbuf(stdout,NULL);
    int *xC,*xAsm,*yC,*yAsm;
    xC = new int[2000];
    xAsm = new int[2000];
    yC = new int[2000];
    yAsm = new int[2000];
    for(int i = 0; i < 2000; i++)
    {
        xC[i] = 0;
        xAsm[i] = 0;
        yC[i] = i;
        yAsm[i] = i;
    }
    time_t start = clock();
    calcuC(xC,yC,2000);

    //    calcuAsm(xAsm,yAsm,2000);
    //    for(int i = 0; i < 2000; i++)
    //    {
    //        if(xC[i] != xAsm[i])
    //        {
    //            cout<<"xC["<<i<<"]="<<xC[i]<<" "<<"xAsm["<<i<<"]="<<xAsm[i]<<endl;
    //            errorOccured = true;
    //            break;
    //        }
    //    }
    //    if(errorOccured)
    //        cout<<"Error occurs!"<<endl;
    //    else
    //        cout<<"Works fine!"<<endl;

    time_t end = clock();

    //    cout<<"time = "<<(float)(end - start) / CLOCKS_PER_SEC<<"\n";

    cout<<"time = "<<end - start<<endl;
    return 0;
}

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

И тут приходит результат.

Функция сборки версии:

Debug   Release
---------------
732        668
733        680
659        672
667        675
684        694
Average:   677

Функция версии C++:

Debug     Release
-----------------
1068      168
 999      166
1072      231
1002      166
1114      183
Average:  182

Код C++ в режиме выпуска почти в 3,7 раза быстрее кода сборки. Зачем?

Я полагаю, что код сборки, который я написал, не так эффективен, как код, сгенерированный GCC. Обычному программисту, как я, трудно писать код быстрее, чем его оппонент, сгенерированный компилятором. Значит ли это, что я не должен доверять производительности написанного моими руками ассемблера, сосредоточиться на C++ и забыть о языке ассемблера?

22 ответа

Решение

Да, в большинстве случаев.

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

Вы всегда можете создать пример, в котором ручной ассемблерный код лучше скомпилированного кода, но обычно это вымышленный пример или отдельная подпрограмма, а не настоящая программа, содержащая более 500000 строк кода C++). Я думаю, что компиляторы будут производить лучший ассемблерный код 95% раз, а иногда, только в редких случаях, вам может понадобиться написать ассемблерный код для нескольких коротких, часто используемых, критичных к производительности подпрограмм или когда вам нужно получить доступ к функциям вашего любимого языка высокого уровня не выставляет. Хотите прикосновения этой сложности? Прочитайте этот удивительный ответ здесь на SO.

Почему это?

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

Когда вы пишете код на ассемблере, вы должны создавать четко определенные функции с четко определенным интерфейсом вызовов. Однако они могут принимать во внимание оптимизацию всей программы и межпроцедурную оптимизацию, такую ​​как распределение регистров, постоянное распространение, удаление общего подвыражения, планирование команд и другие сложные, неочевидные оптимизации (например, модель многогранника). Об архитектуре RISC ребята перестали беспокоиться об этом много лет назад (например, планирование инструкций очень трудно настраивать вручную), а современные ЦПУ CISC также имеют очень длинные конвейеры.

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

Иногда компиляторы могут автоматически использовать некоторые инструкции MMX/SIMDx, и если вы их не используете, вы просто не можете их сравнивать (другие ответы уже хорошо рассмотрели ваш код сборки). Просто для циклов это краткий список оптимизаций цикла того, что обычно проверяется компилятором (как вы думаете, вы могли бы сделать это самостоятельно, когда ваш график был выбран для программы на C#?) Если вы пишете что-то в сборке, я думаю, что вы должны рассмотреть хотя бы несколько простых оптимизаций. Пример учебника для массивов - развернуть цикл (его размер известен во время компиляции). Сделайте это и запустите тест снова.

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

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

Все это говорит о том, что даже когда вы можете создавать сборочный код в 5-10 раз быстрее, вам следует спросить своих клиентов, предпочитают ли они платить одну неделю вашего времени или покупать процессор на 50$ быстрее. Чрезвычайная оптимизация чаще всего (и особенно в приложениях LOB) просто не требуется от большинства из нас.

Ваш ассемблерный код исключительно плохой, немного неоптимальный и может быть улучшен:

  • Вы нажимаете и выталкиваете регистр ( EDX) во внутреннем цикле. Это должно быть удалено из цикла.
  • Вы перезагружаете указатели массива на каждой итерации цикла. Это должно выйти из цикла.
  • Вы используете loop инструкция, которая, как известно, очень медленная на большинстве современных процессоров (возможно, в результате использования древней сборочной книги *)
  • Вы не пользуетесь возможностью ручного раскручивания петли.
  • Вы не используете доступные инструкции SIMD.

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

* Конечно, я не знаю, действительно ли вы получили loop инструкция из древней сборочной книги. Но вы почти никогда не увидите его в коде реального мира, так как каждый компилятор достаточно умен, чтобы не излучать loopВидишь только в ИМХО плохих и устаревших книгах.

Даже до углубления в сборку существуют преобразования кода, которые существуют на более высоком уровне.

static int const TIMES = 100000;

void calcuC(int *x, int *y, int length) {
  for (int i = 0; i < TIMES; i++) {
    for (int j = 0; j < length; j++) {
      x[j] += y[j];
    }
  }
}

может быть преобразован в Loop Rotation:

static int const TIMES = 100000;

void calcuC(int *x, int *y, int length) {
    for (int j = 0; j < length; ++j) {
      for (int i = 0; i < TIMES; ++i) {
        x[j] += y[j];
      }
    }
}

что намного лучше, насколько локальность памяти идет.

Это может быть оптимизировано дальше, делая a += b X раз эквивалентно выполнению a += X * b Итак, мы получаем:

static int const TIMES = 100000;

void calcuC(int *x, int *y, int length) {
    for (int j = 0; j < length; ++j) {
      x[j] += TIMES * y[j];
    }
}

однако, кажется, мой любимый оптимизатор (LLVM) не выполняет это преобразование.

[править] Я обнаружил, что преобразование выполняется, если у нас было restrict квалификатор к x а также y, Действительно без этого ограничения, x[j] а также y[j] может псевдоним в том же месте, что делает это преобразование ошибочным. [конец редактирования]

Во всяком случае, это, я думаю, оптимизированная версия C. Уже намного проще. Основываясь на этом, вот мой взлом в ASM (я позволил Clang генерировать его, я бесполезен в этом):

calcuAsm:                               # @calcuAsm
.Ltmp0:
    .cfi_startproc
# BB#0:
    testl   %edx, %edx
    jle .LBB0_2
    .align  16, 0x90
.LBB0_1:                                # %.lr.ph
                                        # =>This Inner Loop Header: Depth=1
    imull   $100000, (%rsi), %eax   # imm = 0x186A0
    addl    %eax, (%rdi)
    addq    $4, %rsi
    addq    $4, %rdi
    decl    %edx
    jne .LBB0_1
.LBB0_2:                                # %._crit_edge
    ret
.Ltmp1:
    .size   calcuAsm, .Ltmp1-calcuAsm
.Ltmp2:
    .cfi_endproc

Боюсь, я не понимаю, откуда берутся все эти инструкции, однако вы всегда можете повеселиться и попробовать сравнить их... но я все равно буду использовать оптимизированную версию C, а не сборочную, в коде, гораздо более портативный.

Краткий ответ: да.

Длинный ответ: да, если вы действительно не знаете, что делаете, и у вас есть причина для этого.

Я исправил свой код asm:

  __asm
{   
    mov ebx,TIMES
 start:
    mov ecx,lengthOfArray
    mov esi,x
    shr ecx,1
    mov edi,y
label:
    movq mm0,QWORD PTR[esi]
    paddd mm0,QWORD PTR[edi]
    add edi,8
    movq QWORD PTR[esi],mm0
    add esi,8
    dec ecx 
    jnz label
    dec ebx
    jnz start
};

Результаты для релизной версии:

 Function of assembly version: 81
 Function of C++ version: 161

Код сборки в режиме выпуска почти в 2 раза быстрее, чем C++.

Означает ли это, что я не должен доверять производительности языка ассемблера, написанного моими руками

Да, это именно то, что это значит, и это верно для каждого языка. Если вы не знаете, как писать эффективный код на языке X, то вы не должны доверять своей способности писать эффективный код на X. И поэтому, если вы хотите эффективный код, вам следует использовать другой язык.

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

В настоящее время единственной причиной использования ассемблера является использование некоторых функций, недоступных языку.

Это относится к:

  • Программирование ядра, которое требует доступа к определенным аппаратным функциям, таким как MMU
  • Высокопроизводительное программирование, которое использует очень специфические векторные или мультимедийные инструкции, не поддерживаемые вашим компилятором.

Но современные компиляторы довольно умны, они могут даже заменить два отдельных оператора, таких какd = a / b; r = a % b; с единственной инструкцией, которая вычисляет деление и остаток за один раз, если это возможно, даже если C не имеет такого оператора.

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

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

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

Для вдохновения я бы порекомендовал вам посмотреть статьи Майкла Абраша (если вы не слышали о нем, он - гуру оптимизации; он даже сотрудничал с Джоном Кармаком в оптимизации программного рендеринга Quake!)

"нет самого быстрого кода", - Майкл Абраш

Я изменил код asm:

 __asm
{ 
    mov ebx,TIMES
 start:
    mov ecx,lengthOfArray
    mov esi,x
    shr ecx,2
    mov edi,y
label:
    mov eax,DWORD PTR [esi]
    add eax,DWORD PTR [edi]
    add edi,4   
    dec ecx 
    mov DWORD PTR [esi],eax
    add esi,4
    test ecx,ecx
    jnz label
    dec ebx
    test ebx,ebx
    jnz start
};

Результаты для релизной версии:

 Function of assembly version: 41
 Function of C++ version: 161

Код сборки в режиме выпуска почти в 4 раза быстрее, чем C++. IMHo, скорость сборки кода зависит от программиста

Это очень интересная тема!
Я сменил MMX по SSE в коде Саши
Вот мои результаты:

Function of C++ version:      315
Function of assembly(simply): 312
Function of assembly  (MMX):  136
Function of assembly  (SSE):  62

Ассемблерный код с SSE в 5 раз быстрее, чем C++

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

Просто например, даже в том, что я не уверен, что это больше правильно:)

Выполнение:

mov eax,0

стоить больше циклов, чем

xor eax,eax

который делает то же самое.

Компилятор знает все эти хитрости и использует их.

Компилятор победил тебя. Я попробую, но не буду давать никаких гарантий. Я предполагаю, что "умножение" на ВРЕМЕНИ призвано сделать его более актуальным тестом производительности, что y а также x 16 выровнены, и это length ненулевое кратное 4. Это, вероятно, все равно так или иначе.

  mov ecx,length
  lea esi,[y+4*ecx]
  lea edi,[x+4*ecx]
  neg ecx
loop:
  movdqa xmm0,[esi+4*ecx]
  paddd xmm0,[edi+4*ecx]
  movdqa [edi+4*ecx],xmm0
  add ecx,4
  jnz loop

Как я уже сказал, я не даю никаких гарантий. Но я буду удивлен, если это будет сделано намного быстрее - узким местом здесь является пропускная способность памяти, даже если все это удар L1.

Просто слепая реализация того же самого алгоритма, инструкция за инструкцией, в сборке гарантированно будет медленнее, чем то, что может сделать компилятор.

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

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

Как компилятор я бы заменил цикл с фиксированным размером на множество задач исполнения.

int a = 10;
for (int i = 0; i < 3; i += 1) {
    a = a + i;
}

будет производить

int a = 10;
a = a + 0;
a = a + 1;
a = a + 2;

и в конце концов он узнает, что "a = a + 0;" бесполезен, поэтому он удалит эту строку. Надеюсь, что-то в вашей голове теперь готовы прикрепить некоторые варианты оптимизации в качестве комментария. Все эти очень эффективные оптимизации сделают скомпилированный язык быстрее.

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

Аналогично, тот же принцип применяется, когда вы поднимаетесь по иерархии языковой абстракции. Да, вы можете написать синтаксический анализатор на C так же быстро, как быстрый и грязный Perl-скрипт, и многие это делают. Но это не значит, что, поскольку вы использовали C, ваш код будет быстрым. Во многих случаях языки высокого уровня выполняют оптимизации, которые вы, возможно, даже не рассматривали.

Это именно то, что это значит. Оставьте микро-оптимизации для компилятора.

Во многих случаях оптимальный способ выполнения некоторой задачи может зависеть от контекста, в котором выполняется задача. Если подпрограмма написана на ассемблере, последовательность команд, как правило, не может быть изменена в зависимости от контекста. В качестве простого примера рассмотрим следующий простой метод:

inline void set_port_high(void)
{
  (*((volatile unsigned char*)0x40001204) = 0xFF);
}

Компилятор для 32-битного кода ARM, учитывая вышеизложенное, скорее всего, отобразит его примерно так:

ldr  r0,=0x40001204
mov  r1,#0
strb r1,[r0]
[a fourth word somewhere holding the constant 0x40001204]

или возможно

ldr  r0,=0x40001000  ; Some assemblers like to round pointer loads to multiples of 4096
mov  r1,#0
strb r1,[r0+0x204]
[a fourth word somewhere holding the constant 0x40001000]

Это может быть немного оптимизировано в собранном вручную коде, например:

ldr  r0,=0x400011FF
strb r0,[r0+5]
[a third word somewhere holding the constant 0x400011FF]

или же

mvn  r0,#0xC0       ; Load with 0x3FFFFFFF
add  r0,r0,#0x1200  ; Add 0x1200, yielding 0x400011FF
strb r0,[r0+5]

Оба из собранных вручную подходов потребовали бы 12 байтов кода, а не 16; последний заменит "нагрузку" на "добавление", что на ARM7-TDMI выполнит два цикла быстрее. Если бы код собирался выполняться в контексте, где r0 не знал / не заботился, версии на ассемблере были бы несколько лучше, чем скомпилированная версия. С другой стороны, предположим, что компилятор знал, что какой-то регистр [например, r5] будет содержать значение, которое было в пределах 2047 байтов от желаемого адреса 0x40001204 [например, 0x40001000], и далее знал, что какой-то другой регистр [например, r7] собирается хранить значение, младшие биты которого были 0xFF. В этом случае компилятор может оптимизировать C-версию кода, чтобы просто:

strb r7,[r5+0x204]

Гораздо короче и быстрее, чем даже оптимизированный вручную код сборки. Далее, предположим, что set_port_high произошел в контексте:

int temp = function1();
set_port_high();
function2(temp); // Assume temp is not used after this

Совсем неправдоподобно при кодировании для встроенной системы. Если set_port_high написан на ассемблере, компилятор должен будет переместить r0 (который содержит возвращаемое значение из function1) в другом месте, прежде чем вызывать ассемблерный код, а затем переместить это значение обратно в r0 (так как function2 будет ожидать своего первого параметра в r0), поэтому для "оптимизированного" кода сборки потребуется пять инструкций. Даже если компилятор не знает ни одного регистра, содержащего адрес или значение для хранения, его версия с четырьмя инструкциями (которую он может адаптировать для использования любых доступных регистров - не обязательно r0 и r1) превзойдет "оптимизированную" сборку языковая версия. Если компилятор имел необходимые адрес и данные в r5 и r7, как описано ранее, function1 не будет изменять эти регистры, и, следовательно, он может заменить set_port_high с одним strb инструкция -четыре инструкции меньше и быстрее, чем "оптимизированный вручную" код сборки.

Обратите внимание, что оптимизированный вручную ассемблерный код может часто превосходить компилятор в тех случаях, когда программист знает точный поток программы, но компиляторы работают лучше в тех случаях, когда фрагмент кода написан до того, как известен его контекст, или когда один фрагмент исходного кода может быть вызывается из нескольких контекстов [если set_port_high используется в пятидесяти различных местах кода, компилятор может самостоятельно решить для каждого из них, как лучше его расширить].

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

[Незначительное замечание: в некоторых местах ассемблерный код может использоваться для создания гипероптимизированного тупого беспорядка; например, один фрагмент кода, который я сделал для ARM, должен был извлечь слово из ОЗУ и выполнить одну из примерно двенадцати подпрограмм, основанных на верхних шести битах значения (многие значения сопоставлены одной и той же подпрограмме). Я думаю, что я оптимизировал этот код до чего-то вроде:

ldrh  r0,[r1],#2! ; Fetch with post-increment
ldrb  r1,[r8,r0 asr #10]
sub   pc,r8,r1,asl #2

Регистр r8 всегда содержал адрес основной таблицы диспетчеризации (в цикле, где код тратит 98% своего времени, ничто никогда не использовало его для каких-либо других целей); все 64 записи относятся к адресам в 256 байтах, предшествующих ему. Поскольку основной цикл имел в большинстве случаев жесткий предел времени выполнения около 60 циклов, выборка и отправка из девяти циклов были очень полезны для достижения этой цели. Использование таблицы из 256 32-битных адресов было бы на один цикл быстрее, но поглотило бы 1 КБ очень ценной оперативной памяти [флэш-память добавила бы более одного состояния ожидания]. Использование 64 32-битных адресов потребовало бы добавления инструкции для маскировки некоторых битов из извлеченного слова, и все равно поглотило бы на 192 байт больше, чем таблица, которую я фактически использовал. Использование таблицы 8-битных смещений позволило получить очень компактный и быстрый код, но я не ожидал, что компилятор когда-нибудь придумает; Я также не ожидал бы, что компилятор выделит регистр "полный рабочий день" для хранения адреса таблицы.

Приведенный выше код был разработан для работы в качестве автономной системы; он мог бы периодически вызывать код C, но только в определенные моменты времени, когда аппаратное обеспечение, с которым оно взаимодействовало, могло безопасно переводиться в состояние "ожидания" на два интервала примерно в одну миллисекунду каждые 16 мс.

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

C++ работает быстрее, если вы не используете правильный язык ассемблера с более глубокими знаниями.

Когда я кодирую в ASM, я реорганизую инструкции вручную, чтобы ЦП мог выполнять больше их параллельно, когда это логически возможно. Например, я почти не использую ОЗУ, когда кодирую в ASM: в ASM может быть более 20000 строк кода, и я никогда не использовал push / pop.

Вы можете потенциально перейти в середину кода операции, чтобы самостоятельно модифицировать код и поведение без возможного штрафа за самоизменяющийся код. Доступ к регистрам занимает 1 такт (иногда занимает 0,25 тактов) ЦП. Доступ к ОЗУ может занять сотни.

В моем последнем приключении с ASM я ни разу не использовал ОЗУ для хранения переменной (на тысячи строк ASM). ASM может быть потенциально невообразимо быстрее, чем C++. Но это зависит от множества переменных факторов, таких как:

1. I was writing my apps to run on the bare metal.
2. I was writing my own boot loader that was starting my programs in ASM so there was no OS management in the middle.

Сейчас я изучаю C# и C++, потому что я понял, что производительность имеет значение!! В свободное время вы можете попытаться создать самые быстрые из возможных программ, используя только ASM. Но чтобы что-то производить, используйте язык высокого уровня.

Например, последняя программа, которую я написал, использовала JS и GLSL, и я никогда не замечал проблем с производительностью, даже говоря о JS, которая работает медленно. Это потому, что простая концепция программирования GPU для 3D делает скорость языка, который посылает команды в GPU, почти неактуальной.

Скорость только ассемблера на голом металле неопровержима. Может ли это быть еще медленнее в C++? - Это может быть потому, что вы пишете ассемблерный код с компилятором, не использующим ассемблер для начала.

Мой личный совет - никогда не писать ассемблерный код, если вы можете избежать его, даже если я люблю ассемблер.

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

Компилятор C++ после оптимизации на организационном уровне будет генерировать код, который будет использовать встроенные функции целевого процессора. HLL никогда не будет опережать или превосходить ассемблер по нескольким причинам; 1.) HLL будет скомпилирован и выведен с кодом Accessor, проверкой границ и, возможно, встроенным сборщиком мусора (ранее адресовавшим область действия в манере ООП), все требующие циклов (триггеры и флопсы). В наши дни HLL отлично справляется со своей задачей (включая более новый C++ и другие, такие как GO), но если они превосходят ассемблер (а именно ваш код), вам необходимо обратиться к документации по процессору - сравнения с неаккуратным кодом, безусловно, неубедительны, и компилируемые языки, такие как ассемблер, все решают вплоть до кода операции HLL абстрагирует детали и не устраняет их, иначе приложение не будет запущено, даже если оно будет распознано операционной системой хоста.

Большая часть кода на ассемблере (прежде всего объекты) выводится как "безголовый" для включения в другие исполняемые форматы с гораздо меньшими затратами на обработку, следовательно, это будет намного быстрее, но гораздо более небезопасно; если исполняемый файл выводится ассемблером (NAsm, YAsm и т. д.), он все равно будет работать быстрее, пока полностью не совпадет с HLL-кодом по функциональности, тогда результаты могут быть точно взвешены.

Вызов объекта кода на основе ассемблера из HLL в любом формате неизбежно увеличит накладные расходы обработки, а также вызовы пространства памяти, использующие глобально распределенную память для переменных / постоянных типов данных (это относится как к LLL, так и к HLL). Помните, что конечный результат в конечном итоге использует процессор в качестве его api и abi относительно аппаратного обеспечения (код операции), и ассемблеры, и "компиляторы HLL" по существу / принципиально идентичны, с единственным истинным исключением является читаемость (грамматическая).

Консольное приложение Hello world в ассемблере, использующем FAsm, имеет размер 1,5 КБ (а в Windows еще меньше в FreeBSD и Linux) и превосходит все, что GCC может выбросить в свой лучший день; Причины - неявное заполнение с помощью nops, проверка доступа и проверка границ. Настоящая цель - чистые библиотеки HLL и оптимизируемый компилятор, который нацелен на процессор "хардкорным" способом, и большинство делает это в наши дни (наконец). GCC не лучше, чем YAsm - это практика кодирования и понимание разработчика, которые находятся под вопросом, и "оптимизация" приходит после изучения новичков и промежуточного обучения и опыта.

Компиляторы должны связывать и собирать для вывода в том же коде операции, что и ассемблер, потому что эти коды - это все, что ЦП, кроме (CISC или RISC [PIC тоже]). YAsm оптимизировал и значительно очистил ранние NAsm, что в итоге ускорило весь вывод этого ассемблера, но даже тогда YAsm, как и NAsm, по-прежнему создает исполняемые файлы с внешними зависимостями, ориентируясь на библиотеки ОС от имени разработчика, поэтому пробег может варьироваться. В заключение, C++ невероятно и гораздо более безопасен, чем ассемблер на 80+ процентов, особенно в коммерческом секторе...

Сборка может быть быстрее, если ваш компилятор генерирует много кода поддержки OO.

Редактировать:

Для downvoters: ОП написал "я должен... сосредоточиться на C++ и забыть о ассемблере?" и я поддерживаю мой ответ. Вы всегда должны следить за кодом, генерируемым ОО, особенно при использовании методов. Не забывать о языке ассемблера означает, что вы будете периодически просматривать сборку, которую генерирует ваш ОО-код, который, я считаю, является обязательным для написания хорошо работающего программного обеспечения.

На самом деле, это относится ко всему компилируемому коду, а не только к ОО.

Другие вопросы по тегам