Является ли язык встроенной сборки медленнее, чем собственный код на С++?
Я попытался сравнить производительность встроенного языка ассемблера и кода на С++, поэтому я написал функцию, которая добавляет два массива размером 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
Функция версии С++:
Debug Release
-----------------
1068 168
999 166
1072 231
1002 166
1114 183
Average: 182
Код С++ в режиме выпуска почти в 3,7 раза быстрее, чем код сборки. Почему?
Я предполагаю, что код сборки, который я написал, не так эффективен, как код, созданный GCC. Трудно для обычного программиста, как я, писать код быстрее, чем его противник, сгенерированный компилятором. Это означает, что я не должен доверять производительности языка ассемблера, написанного моими руками, сосредоточиться на С++ и забыть о языке ассемблера?
Ответы
Ответ 1
Да, чаще всего.
Прежде всего вы начинаете с неправильного предположения, что язык низкого уровня (сборка в этом случае) всегда будет генерировать более быстрый код, чем язык высокого уровня (в этом случае С++ и C). Это неправда. Является ли код C всегда быстрее, чем Java-код? Нет, потому что есть другая переменная: программист. То, как вы пишете код и знание деталей архитектуры, значительно влияет на производительность (как вы видели в этом случае).
Вы можете всегда создавать пример, когда код сборки ручной работы лучше, чем скомпилированный код, но обычно - это вымышленный пример или отдельная процедура, а не настоящая программа из 500 000 строк кода С++). Я думаю, что компиляторы будут генерировать лучший код сборки 95% и иногда, только в редкие времена, вам может понадобиться написать код сборки для нескольких, коротких, с высокой степенью использования, критически важные параметры или когда вам нужно получить доступ к функциям, которые ваш любимый язык высокого уровня не раскрывает. Вы хотите прикоснуться к этой сложности? Прочитайте этот удивительный ответ здесь, на SO.
Зачем это?
Прежде всего потому, что компиляторы могут делать оптимизации, которые мы даже не можем себе представить (см. этот короткий список), и они будут делать их в секундах (когда нам могут понадобиться дни).
Когда вы создаете код в сборке, вы должны делать четко определенные функции с четко определенным интерфейсом вызова. Однако они могут принимать во внимание оптимизацию всей программы и межпроцедурную оптимизацию, такую
как распределение регистров, постоянное распространение, устранение общего подвыражения, планирование инструкций и другие сложные, не очевидные оптимизации (модель Политипа, например). На RISC архитектура ребята перестали беспокоиться об этом много лет назад (например, для планирования команд, например, очень сложно настроить вручную) и современных CISC Процессоры имеют очень длинный трубопроводы.
Для некоторых сложных микроконтроллеров даже системные библиотеки записываются на C вместо сборки, потому что их компиляторы производят лучший (и простой в обслуживании) окончательный код.
Компиляторы иногда могут самостоятельно использовать некоторые инструкции MMX/SIMDx, и если вы их не используете, вы просто не можете сравнивать ( другие ответы уже очень хорошо рассмотрели ваш код сборки).
Просто для циклов это короткий список оптимизаций циклов того, что обычно проверяется компилятором (как вы думаете вы могли бы сделать это сами, когда ваше расписание было принято для программы С#?) Если вы пишете что-то в сборке, я думаю, вам нужно рассмотреть хотя бы некоторые simple оптимизации. Пример школьной книги для массивов - развернуть цикл (его размер известен во время компиляции). Сделайте это и повторите тест.
В наши дни также очень редко нужно использовать язык ассемблера по другой причине: множество разных процессоров. Вы хотите поддержать их всех? Каждый из них имеет конкретный microarchitecture и некоторые определенные наборы инструкций, У них разное количество функциональных блоков, и инструкции по сборке должны быть организованы так, чтобы все они были заняты. Если вы пишете на C, вы можете использовать PGO, но в сборке вам понадобятся большие знания этой конкретной архитектуры (и переосмыслить и переделать все для другой архитектуры). Для небольших задач компилятор обычно делает это лучше, и для сложных задач обычно работа не возвращается (и компилятор может сделать лучше в любом случае).
Если вы сядете, и вы посмотрите на свой код, вероятно, вы увидите, что получите больше, чтобы перепроектировать алгоритм, чем перевести на сборку (прочитайте эту отличную статью здесь на SO), есть оптимизация на высоком уровне (и подсказки для компилятора), которые вы можете эффективно применить, прежде чем прибегать к ассемблеру. Вероятно, стоит упомянуть, что часто используя встроенные функции, вы получите прирост производительности, который вам нужен, и компилятор по-прежнему сможет выполнять большую часть своих оптимизаций.
Все это сказало, даже если вы можете создать код сборки более чем в 5-10 раз быстрее, вы должны спросить своих клиентов, предпочитают ли они платить одну неделю вашего времени или купить 50% быстрее CPU. Чрезвычайная оптимизация чаще всего (и особенно в LOB-приложениях) просто не требуется от большинства из нас.
Ответ 2
Ваш ассемблерный код неоптимален и может быть улучшен:
- Вы нажимаете и выталкиваете регистр (EDX) во внутреннем цикле. Это должно быть удалено из цикла.
- Вы перезагружаете указатели массива на каждой итерации цикла. Это должно выйти из цикла.
- Вы используете инструкцию
loop
, которая, как известно, очень медленная на большинстве современных процессоров (возможно, в результате использования древней сборочной книги *) - Вы не пользуетесь возможностью ручного раскручивания циклы.
- Вы не используете доступные инструкции SIMD.
Поэтому, если вы не значительно улучшите свои навыки в отношении ассемблера, вам не имеет смысла писать код на ассемблере для повышения производительности.
* Конечно, я не знаю, действительно ли вы получили инструкцию по loop
из древней сборочной книги. Но вы почти никогда не видите его в коде реального мира, так как каждый компилятор достаточно умен, чтобы не создавать loop
, вы видите это только в ИМХО плохих и устаревших книгах.
Ответ 3
Даже до вникания в сборку существуют преобразования кода, которые существуют на более высоком уровне.
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) не выполняет это преобразование.
[edit] Я обнаружил, что преобразование выполняется, если у нас был restrict
классификатор до x
и y
. Действительно, без этого ограничения x[j]
и y[j]
могли бы быть псевдонимом в том же месте, что делает это преобразование ошибочным. [end edit]
В любом случае, это, я думаю, оптимизированная версия 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, а не сборку, в коде, гораздо более портативный.
Ответ 4
Короткий ответ: да.
Длинный ответ: да, если вы действительно не знаете, что делаете, и у вас есть повод сделать это.
Ответ 5
Я установил код 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
};
Результаты для версии Release:
Function of assembly version: 81
Function of C++ version: 161
Код сборки в режиме выпуска почти в 2 раза быстрее, чем С++.
Ответ 6
Означает ли это, что я не должен доверять производительности языка ассемблера, написанного руками?
Да, это именно то, что это значит, и это верно для каждого языка. Если вы не знаете, как писать эффективный код на языке X, то вам не следует доверять своей способности писать эффективный код в X. И поэтому, если вам нужен эффективный код, вы должны использовать другой язык.
Ассамблея особенно чувствительна к этому, потому что, ну, что вы видите, это то, что вы получаете. Вы пишете конкретные инструкции, которые вы хотите выполнить ЦП. С языками высокого уровня существует один компилятор, который может преобразовать ваш код и удалить многие неэффективности. С сборкой вы сами.
Ответ 7
Единственной причиной использования языка ассемблера в настоящее время является использование некоторых функций, недоступных для языка.
Это относится к:
- Программирование ядра, которое должно иметь доступ к определенным аппаратным функциям, таким как MMU
- Высокопроизводительное программирование, которое использует очень специфические векторные или мультимедийные инструкции, не поддерживаемые вашим компилятором.
Но современные компиляторы довольно умны, они могут даже заменить два отдельных оператора, например
d = a / b; r = a % b;
с одной инструкцией, которая вычисляет деление и остаток за один раз, если он доступен, даже если C не имеет такого оператора.
Ответ 8
Верно, что современный компилятор отлично справляется с оптимизацией кода, но я все же призываю вас продолжать изучение сборки.
Прежде всего, вы явно не запуганы им, что отличный, отличный плюс, затем - вы на правильном пути с помощью профилирования, чтобы проверить или отменить свою скорость предположения, вы запрашиваете вход от опытных людей, и у вас есть самый большой инструмент для оптимизации, известный человечеству: мозг.
По мере увеличения вашего опыта вы узнаете, когда и где его использовать (как правило, самые плотные, самые внутренние петли в вашем коде, после того, как вы глубоко оптимизировали алгоритмический уровень).
Для вдохновения я бы рекомендовал вам искать статьи Майкл Абраш (если вы не слышали от него, он - оптимизационный гуру, он даже сотрудничал с Джоном Кармаком в оптимизации программного обеспечения Quake Software renderer!)
"нет такого понятия, как самый быстрый код" - Майкл Абраш
Ответ 9
Я изменил код 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
};
Результаты для версии Release:
Function of assembly version: 41
Function of C++ version: 161
Код сборки в режиме выпуска почти в 4 раза быстрее, чем С++.
IMHo, скорость кода сборки зависит от Programmer
Ответ 10
Большинство высокоуровневых компиляторов языков очень оптимизированы и знают, что они делают. Вы можете попробовать сбросить код дизассемблирования и сравнить его с собственной сборкой. Я считаю, что вы увидите некоторые интересные трюки, которые использует ваш компилятор.
Просто, например, даже если я не уверен, что он прав больше:):
Doing:
mov eax,0
стоит больше циклов, чем
xor eax,eax
который делает то же самое.
Компилятор знает все эти трюки и использует их.
Ответ 11
Компилятор избил вас. Я попробую, но я не буду давать никаких гарантий. Я предполагаю, что "умножение" TIMES предназначено для того, чтобы сделать его более релевантным тестом производительности, что 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.
Ответ 12
Это очень интересная тема!
Я изменил MMX на SSE в коде Sasha
Вот мои результаты:
Function of C++ version: 315
Function of assembly(simply): 312
Function of assembly (MMX): 136
Function of assembly (SSE): 62
Ассемблерный код с SSE в 5 раз быстрее, чем С++
Ответ 13
Просто слепо реализовать тот же алгоритм, инструкция по инструкции в сборке гарантирована, чтобы быть медленнее, чем то, что может сделать компилятор.
Это потому, что даже самая маленькая оптимизация, которую делает компилятор, лучше, чем ваш жесткий код без оптимизации вообще.
Конечно, можно бить компилятор, особенно если это небольшая, локализованная часть кода, мне даже пришлось сделать это сам, чтобы получить ок. 4x ускоряется, но в этом случае мы должны в значительной степени полагаться на хорошее знание аппаратного обеспечения и многочисленные, казалось бы, интригующие трюки.
Ответ 14
В качестве компилятора я заменил бы цикл с фиксированным размером на множество задач выполнения.
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;" бесполезно, поэтому он удалит эту строку.
Надеюсь, что-то в вашей голове теперь захочет добавить некоторые варианты оптимизации в качестве комментария. Все эти очень эффективные оптимизации сделают скомпилированный язык более быстрым.
Ответ 15
Это именно то, что это значит. Оставьте микро-оптимизацию компилятору.
Ответ 16
Мне нравится этот пример, потому что он демонстрирует важный урок о низкоуровневом коде. Да, вы можете написать сборку так же быстро, как ваш C-код. Это тавтологически верно, но не обязательно означает ничего. Ясно, что кто-то может, иначе ассемблер не знал бы соответствующей оптимизации.
Аналогично, тот же принцип применяется, когда вы поднимаетесь по иерархии абстракции языка. Да, вы можете написать синтаксический анализатор на C, который так же быстро, как быстрый и грязный perl script, и многие люди это делают. Но это не означает, что, поскольку вы использовали C, ваш код будет быстрым. Во многих случаях языки более высокого уровня делают оптимизации, которые вы, возможно, даже не рассматривали.
Ответ 17
Во многих случаях оптимальный способ выполнения какой-либо задачи может зависеть от контекста, в котором выполняется задача. Если процедура написана на языке ассемблера, как правило, не может быть изменена последовательность инструкций в зависимости от контекста. В качестве простого примера рассмотрим следующий простой метод:
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 КБ очень ценного ОЗУ [flash добавил бы несколько состояний ожидания]. Использование 64 32-разрядных адресов потребовало бы добавления инструкции для маскировки некоторых битов из извлеченного слова и по-прежнему будет поглощать 192 байта, чем та, которую я фактически использовал. Использование таблицы 8-битных смещений дает очень компактный и быстрый код, но не то, что я ожидал бы от компилятора; Я также не ожидал, что компилятор выделит регистр "полный рабочий день" для хранения адреса таблицы.
Вышеупомянутый код был разработан для работы в качестве автономной системы; он может периодически вызывать код C, но только в определенные моменты времени, когда аппаратное обеспечение, с которым он обменивается данными, может безопасно быть переведено в "незанятое" состояние для двух интервалов примерно один миллисекунд каждые 16 мс.
Ответ 18
В последнее время все оптимизаторы скорости, которые я сделал, заменяли медленный код с поврежденным мозгом с помощью разумного кода. Но для вещей скорость была действительно критической, и я прилагал серьезные усилия к тому, чтобы сделать что-то быстро, результат всегда был итеративным процессом, когда каждая итерация давала больше информации о проблеме, ища способы решения проблемы с меньшим количеством операций. Конечная скорость всегда зависела от того, насколько я понял проблему. Если на каком-либо этапе я использовал код сборки или код C, который был слишком оптимизирован, процесс поиска лучшего решения понес бы и конечный результат был бы медленнее.
Ответ 19
С++ быстрее, если вы не используете язык ассемблера с более глубоким знания с правильным способом.
Когда я код в ASM, я реорганизую инструкции вручную, чтобы процессор мог выполнять больше из них параллельно, когда это логически возможно. Я едва использую RAM, когда я кодирую в ASM, например: в ASM может быть 20000+ строк кода, и я ни разу не использовал push/pop.
Вы могли бы прыгнуть в середине кода операции, чтобы самостоятельно изменить код и поведение без возможного штрафа за самомодифицирующийся код. Доступ к регистрам занимает 1 тик (иногда занимает 0,25 тика) процессора. При достижении ОЗУ может потребоваться сотни.
Для моего последнего приключения ASM я никогда не использовал RAM для хранения переменной (для тысяч строк ASM). ASM может быть потенциально невообразимо быстрее, чем С++. Но это зависит от множества переменных факторов, таких как:
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.
Теперь я изучаю С# и С++, потому что я понял проблемы с производительностью!!
Вы можете попытаться сделать самые быстрые мыслимые программы, используя чистую ASM только в свободное время. Но для того, чтобы что-то создать, используйте язык высокого уровня.
Например, последняя кодированная программа я использовала JS и GLSL, и я никогда не замечал никаких проблем с производительностью, даже говоря о JS, которая медленная. Это связано с тем, что простая концепция программирования GPU для 3D делает скорость языка, который посылает команды на GPU, почти не имеет значения.
Скорость одного ассемблера на голом металле неопровержима.
Может ли быть еще медленнее внутри С++? - Возможно, это потому, что вы пишете код сборки с компилятором, не используя ассемблер для начала.
Мой личный совет - никогда не писать код сборки, если вы можете его избежать, хотя мне нравится сборка.
Ответ 20
Все ответы здесь, кажется, исключают один аспект: иногда мы не пишем код для достижения определенной цели, а для простое удовольствие от него. Возможно, экономично не тратить время на это, но, возможно, нет большего удовлетворения, чем избиение самого быстрого оптимизированного фрагмента кода, оптимизированного для компилятора, с альтернативой ASM с ручным заказом.
Ответ 21
Компилятор С++ после оптимизации на организационном уровне создает код, который будет использовать встроенные функции целевого процессора. HLL никогда не будет превышать или выходить из ассемблера по нескольким причинам; 1.) HLL будет скомпилирован и выведен с кодом Accessor, проверкой границ и, возможно, встроенным в сборку мусора (ранее рассматривавшим сферу действия в манере ООП), все из которых требуют циклов (flips and flops). HLL делает отличную работу в эти дни (в том числе новые С++ и другие, такие как GO), но если они превосходят ассемблер (а именно ваш код), вам нужно проконсультироваться с документацией по процессору. Сравнения с неаккуратным кодом, безусловно, неубедительны и скомпилированы, как и все ассемблеры вплоть до op-кода HLL реферат деталей и не устраняет их, иначе вы не будете запускать приложение, даже если оно распознает ОС хоста.
Большинство ассемблерных кодов (в первую очередь объектов) выводятся как "безголовые" для включения в другие исполняемые форматы с гораздо меньшей обработкой, поэтому он будет намного быстрее, но гораздо более незащищен; если исполняемый файл выводится ассемблером (NAsm, YAsm и т.д.), он все равно будет работать быстрее, пока он полностью не совместит с кодом HLL, тогда результаты могут быть точно взвешены.
Вызов объекта кода на основе ассемблера из HLL в любом формате по своей сути добавит дополнительные издержки на обработку, а также вызовы памяти, использующие глобально выделенную память для переменных/константных типов данных (это относится как к LLL, так и к HLL). Помните, что конечный результат использует процессор в конечном счете как его api и abi относительно аппаратного обеспечения (код операции), и оба, сборщики и компиляторы HLL по существу/принципиально идентичны, единственное истинное исключение - читаемость (грамматическая).
Приветственное консольное приложение Hello в ассемблере, использующее FAsm, составляет 1,5 КБ (и это в Windows еще меньше во FreeBSD и Linux) и превосходит все, что GCC может выбросить в лучший день; причины - это неявное заполнение с помощью nops, проверка доступа и проверка границ, чтобы назвать несколько. Реальной целью является чистая библиотека HLL и оптимизируемый компилятор, который нацеливает процессор на "хардкор", и большинство из них делает это в наши дни (наконец). GCC не лучше, чем YAsm - это практика кодирования и понимание разработчика, о котором идет речь, и "оптимизация" приходит после новинок и промежуточной подготовки и опыта.
Компиляторы должны подключаться и собираться для вывода в том же коде операции, что и для ассемблера, потому что эти коды - все, за исключением CPU (CISC или RISC [PIC]). YAsm оптимизировал и сильно усовершенствовал ранний NASM, в конечном счете ускоряя все выходные данные этого ассемблера, но даже тогда YAsm по-прежнему, подобно NAsm, создает исполняемые файлы с внешними зависимостями, ориентированными на библиотеки ОС от имени разработчика, поэтому пробег может отличаться. В заключение С++ находится в точке, которая невероятна и намного безопаснее, чем ассемблер на 80%, особенно в коммерческом секторе...
Ответ 22
Сборка может быть быстрее, если ваш компилятор генерирует много OO код поддержки.
Edit:
В downvoters: OP писал: "Должен ли я... сосредоточиться на С++ и забыть об ассемблере?" и я согласен с ответом. Вам всегда нужно следить за кодом, создаваемым OO, особенно при использовании методов. Не забывая о языке ассемблера, вы будете периодически просматривать сборку, которую генерирует ваш OO-код, который, по моему мнению, необходим для написания хорошо работающего программного обеспечения.
Собственно, это относится ко всему скомпилированному коду, а не только к OO.