programacion - ¿El lenguaje ensamblador en línea es más lento que el código nativo de C++?
programacion assembler arduino (22)
¿Eso significa que no debería confiar en el rendimiento del lenguaje ensamblador escrito por mis manos?
Sí, eso es exactamente lo que significa, y es cierto para todos los idiomas. Si no sabe cómo escribir un código eficiente en el lenguaje X, entonces no debe confiar en su capacidad para escribir código eficiente en X. Por lo tanto, si desea un código eficiente, debe usar otro idioma.
El montaje es particularmente sensible a esto, porque, bueno, lo que ves es lo que obtienes. Usted escribe las instrucciones específicas que desea que ejecute la CPU. Con los lenguajes de alto nivel, hay un compilador entre ellos, que puede transformar su código y eliminar muchas ineficiencias. Con el ensamblaje, estás solo.
Traté de comparar el rendimiento del lenguaje ensamblador en línea y el código C ++, así que escribí una función que agrega dos arreglos de tamaño 2000 por 100000 veces. Aquí está el código:
#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
};
}
Aquí está 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;
}
Luego ejecuto el programa cinco veces para obtener los ciclos del procesador, lo que podría verse como el tiempo. Cada vez que llamo solo a una de las funciones mencionadas arriba.
Y aquí viene el resultado.
Función de la versión de montaje:
Debug Release
---------------
732 668
733 680
659 672
667 675
684 694
Average: 677
Función de la versión C ++:
Debug Release
-----------------
1068 168
999 166
1072 231
1002 166
1114 183
Average: 182
El código de C ++ en modo de lanzamiento es casi 3.7 veces más rápido que el código de ensamblaje. ¿Por qué?
Supongo que el código de ensamblaje que escribí no es tan efectivo como los generados por GCC. Es difícil para un programador común como yo escribir código más rápido que su oponente generado por una compilación. ¿Eso significa que no debería confiar en el rendimiento del lenguaje ensamblador escrito por mis manos, centrarme en C ++ y olvidarme del lenguaje ensamblador?
C++ is faster unless you are using assembly language with deeper knowledge with the correct way.
When I code in ASM, I reorganize the instructions manually so the CPU can execute more of them in parallel when logically possible. I barely use RAM when I code in ASM for example: There could be 20000+ lines of code in ASM and I not ever once used push/pop.
You could potentially jump in the middle of the opcode to self-modify the code and the behavior without the possible penalty of self-modifying code. Accessing registers takes 1 tick(sometimes takes .25 ticks) of the CPU.Accessing the RAM could take hundreds.
For my last ASM adventure, I never once used the RAM to store a variable(for thousands of lines of ASM). ASM could be potentially unimaginably faster than C++. But it depends on a lot of variable factors such as:
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.
I am now learning C# and C++ because i realized productivity matters!! You could try to do the fastest imaginable programs using pure ASM alone in the free time. But in order to produce something, use some high level language.
For example, the last program I coded was using JS and GLSL and I never noticed any performance issue, even speaking about JS which is slow. This is because the mere concept of programming the GPU for 3D makes the speed of the language that sends the commands to the GPU almost irrelevant.
The speed of assembler alone on the bare metal is irrefutable. Could it be even slower inside C++? - It could be because you are writing assembly code with a compiler not using an assembler to start with.
My personal council is to never write assembly code if you can avoid it, even though I love assembly.
Como compilador, reemplazaría un bucle con un tamaño fijo por muchas tareas de ejecución.
int a = 10;
for (int i = 0; i < 3; i += 1) {
a = a + i;
}
Producirá
int a = 10;
a = a + 0;
a = a + 1;
a = a + 2;
y eventualmente sabrá que "a = a + 0"; es inútil por lo que eliminará esta línea. Esperemos que algo en tu cabeza ahora esté dispuesto a adjuntar algunas opciones de optimización como comentario. Todas esas optimizaciones muy efectivas harán que el lenguaje compilado sea más rápido.
El compilador te gana. Lo probaré, pero no daré ninguna garantía. Asumiré que la "multiplicación" de TIMES pretende hacer que sea una prueba de rendimiento más relevante, que x
están alineados en 16, y que la length
es un múltiplo no nulo de 4. Probablemente todo sea cierto de todos modos.
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
Como dije, no hago ninguna garantía. Pero me sorprendería si se puede hacer mucho más rápido: el cuello de botella aquí es el rendimiento de la memoria, incluso si todo es un golpe L1.
En muchos casos, la forma óptima de realizar alguna tarea puede depender del contexto en el que se realiza la tarea. Si una rutina está escrita en lenguaje ensamblador, generalmente no será posible variar la secuencia de instrucciones según el contexto. Como un simple ejemplo, considere el siguiente método simple:
inline void set_port_high(void)
{
(*((volatile unsigned char*)0x40001204) = 0xFF);
}
Un compilador de código ARM de 32 bits, dado lo anterior, probablemente lo renderice como algo así como:
ldr r0,=0x40001204
mov r1,#0
strb r1,[r0]
[a fourth word somewhere holding the constant 0x40001204]
o quizás
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]
Eso podría optimizarse ligeramente en el código ensamblado a mano, ya sea como:
ldr r0,=0x400011FF
strb r0,[r0+5]
[a third word somewhere holding the constant 0x400011FF]
o
mvn r0,#0xC0 ; Load with 0x3FFFFFFF
add r0,r0,#0x1200 ; Add 0x1200, yielding 0x400011FF
strb r0,[r0+5]
Ambos enfoques ensamblados a mano requerirían 12 bytes de espacio de código en lugar de 16; este último reemplazaría una "carga" con un "agregar", que en un ARM7-TDMI ejecutaría dos ciclos más rápido. Si el código se iba a ejecutar en un contexto donde r0 no sabía / no importa, las versiones en lenguaje ensamblador serían algo mejores que la versión compilada. Por otro lado, supongamos que el compilador sabía que algún registro [por ejemplo, r5] iba a contener un valor que estaba dentro de 2047 bytes de la dirección deseada 0x40001204 [por ejemplo, 0x40001000], y además sabía que algún otro registro [por ejemplo, r7] iba para mantener un valor cuyos bits bajos eran 0xFF. En ese caso, un compilador podría optimizar la versión C del código simplemente:
strb r7,[r5+0x204]
Mucho más corto y más rápido que incluso el código de ensamblaje optimizado a mano. Además, supongamos que set_port_high ocurrió en el contexto:
int temp = function1();
set_port_high();
function2(temp); // Assume temp is not used after this
Nada inverosímil cuando se codifica un sistema integrado. Si set_port_high
está escrito en código ensamblador, el compilador tendría que mover r0 (que retiene el valor devuelto de la function1
) en otro lugar antes de invocar el código ensamblador, y luego mover ese valor a r0 después (ya que function2
esperará su primer parámetro en r0), por lo que el código de ensamblado "optimizado" necesitaría cinco instrucciones. Incluso si el compilador no conocía ningún registro que tenga la dirección o el valor para almacenar, su versión de cuatro instrucciones (que podría adaptarse para usar cualquier registro disponible, no necesariamente r0 y r1) vencería al ensamblado "optimizado" -la versión del lenguaje. Si el compilador tenía la dirección y los datos necesarios en r5 y r7 como se describió anteriormente, la function1
no alteraría esos registros, y por lo tanto podría reemplazar set_port_high
con una sola instrucción strb
cuatro instrucciones más pequeñas y más rápidas que el ensamblado "optimizado a mano" código.
Note that hand-optimized assembly code can often outperform a compiler in cases where the programmer knows the precise program flow, but compilers shine in cases where a piece of code is written before its context is known, or where one piece of source code may be invoked from multiple contexts [if set_port_high
is used in fifty different places in the code, the compiler could independently decide for each of those how best to expand it].
In general, I would suggest that assembly language is apt to yield the greatest performance improvements in those cases where each piece of code can be approached from a very limited number of contexts, and is apt to be detrimental to performance in places where a piece of code may be approached from many different contexts. Interestingly (and conveniently) the cases where assembly is most beneficial to performance are often those where the code is most straightforward and easy to read. The places that assembly language code would turn into a gooey mess are often those where writing in assembly would offer the smallest performance benefit.
[Minor note: there are some places where assembly code can be used to yield a hyper-optimized gooey mess; for example, one piece of code I did for the ARM needed to fetch a word from RAM and execute one of about twelve routines based upon the upper six bits of the value (many values mapped to the same routine). I think I optimized that code to something like:
ldrh r0,[r1],#2! ; Fetch with post-increment
ldrb r1,[r8,r0 asr #10]
sub pc,r8,r1,asl #2
The register r8 always held the address of the main dispatch table (within the loop where the code spend 98% of its time, nothing ever used it for any other purpose); all 64 entries referred to addresses in the 256 bytes preceding it. Since the primary loop had in most cases a hard execution-time limit of about 60 cycles, the nine-cycle fetch and dispatch was very instrumental toward meeting that goal. Using a table of 256 32-bit addresses would have been one cycle faster, but would have gobbled up 1KB of very precious RAM [flash would have added more than one wait state]. Using 64 32-bit addresses would have required adding an instruction to mask off some bits from the fetched word, and would still have gobbled up 192 more bytes than the table I actually used. Using the table of 8-bit offsets yielded very compact and fast code, but not something I would expect a compiler would ever come up with; I also would not expect a compiler to dedicate a register "full time" to holding the table address.
The above code was designed to run as a self-contained system; it could periodically call C code, but only at certain times when the hardware with which it was communicating could safely be put into an "idle" state for two roughly-one-millisecond intervals every 16ms.
Es cierto que un compilador moderno hace un trabajo increíble en la optimización de código, sin embargo, aún así lo alentaría a seguir aprendiendo ensamblaje.
En primer lugar, claramente no te sientes intimidado , eso es una gran ventaja: a continuación, estás en el camino correcto mediante el perfil para validar o descartar las suposiciones de velocidad , estás pidiendo la opinión de personas con experiencia , y tú tener la mejor herramienta de optimización conocida por la humanidad: un cerebro .
A medida que aumente su experiencia, aprenderá cuándo y dónde usarlo (generalmente los bucles más cerrados y más internos en su código, después de que haya sido profundamente optimizado en un nivel algorítmico).
Para inspirarte, te recomendaría buscar artículos de Michael Abrash (si no has tenido noticias suyas, es un gurú de la optimización, ¡incluso colaboró con John Carmack en la optimización del renderizador de software Quake!)
"no existe el código más rápido" - Michael Abrash
Es exactamente lo que significa. Deje las micro-optimizaciones al compilador.
He arreglado mi código 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
};
Resultados para la versión de lanzamiento:
Function of assembly version: 81
Function of C++ version: 161
El código de ensamblado en modo de lanzamiento es casi 2 veces más rápido que C ++.
He cambiado el código 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
};
Resultados para la versión de lanzamiento:
Function of assembly version: 41
Function of C++ version: 161
El código de ensamblado en modo de lanzamiento es casi 4 veces más rápido que C ++. IMHo, la velocidad del código de ensamblaje depende del Programador
Incluso antes de profundizar en el ensamblaje, existen transformaciones de código que existen en un nivel superior.
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];
}
}
}
puede transformarse en via 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];
}
}
}
que es mucho mejor en lo que respecta a la localidad de memoria.
Esto podría optimizarse aún más, hacer a += b
X veces es equivalente a hacer a += X * b
así que obtenemos:
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];
}
}
sin embargo, parece que mi optimizador favorito (LLVM) no realiza esta transformación.
[edit] Descubrí que la transformación se realiza si teníamos el restrict
calificador para y
. De hecho, sin esta restricción, x[j]
y y[j]
podrían alias a la misma ubicación que hace que esta transformación sea errónea. [editar final]
De todos modos, esta es, creo, la versión C optimizada. Ya es mucho más simple. En base a esto, aquí está mi crack en ASM (dejo que Clang lo genere, soy inútil al respecto):
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
Me temo que no entiendo de dónde provienen todas esas instrucciones, sin embargo siempre puedes divertirte y probar y ver cómo se compara ... pero igual usaría la versión C optimizada en lugar de la de ensamblaje, en código, mucho más portátil.
La única razón para utilizar el lenguaje ensamblador hoy en día es usar algunas funciones a las que no puede acceder el idioma.
Esto aplica a:
- Programación Kernel que necesita acceder a ciertas características de hardware como la MMU
- Programación de alto rendimiento que utiliza instrucciones vectoriales o multimedia muy específicas no admitidas por su compilador.
Pero los compiladores actuales son bastante inteligentes, incluso pueden reemplazar dos declaraciones separadas como d = a / b; r = a % b;
d = a / b; r = a % b;
con una sola instrucción que calcula la división y el resto de una vez si está disponible, incluso si C no tiene dicho operador.
La mayoría de los compiladores de idiomas de alto nivel están muy optimizados y saben lo que están haciendo. Puede probar y volcar el código de desmontaje y compararlo con su ensamblaje original. Creo que verás algunos buenos trucos que está usando tu compilador.
Solo por ejemplo, incluso si no estoy seguro de que sea correcto :):
Obra:
mov eax,0
cuesta más ciclos que
xor eax,eax
que hace lo mismo
El compilador conoce todos estos trucos y los usa.
Me encanta este ejemplo porque demuestra una lección importante sobre el código de bajo nivel. Sí, puede escribir el ensamblaje tan rápido como su código C. Esto es tautológicamente cierto, pero no significa necesariamente nada. Claramente, alguien puede, de lo contrario, el ensamblador no sabría las optimizaciones adecuadas.
Del mismo modo, el mismo principio se aplica a medida que sube la jerarquía de la abstracción del lenguaje. Sí, puede escribir un analizador sintáctico en C que sea tan rápido como un script de perl rápido y sucio, y muchas personas lo hacen. Pero eso no significa que porque usaste C, tu código será rápido. En muchos casos, los idiomas de alto nivel hacen optimizaciones que quizás nunca hayas considerado.
Sí, la mayoría de las veces.
En primer lugar, se parte de la suposición errónea de que un lenguaje de bajo nivel (ensamblado en este caso) siempre producirá un código más rápido que el lenguaje de alto nivel (C ++ y C en este caso). No es verdad. ¿El código C siempre es más rápido que el código Java? No porque hay otra variable: programador. La forma en que escribe el código y el conocimiento de los detalles de la arquitectura influye enormemente en el rendimiento (como vio en este caso).
Siempre puedes producir un ejemplo en el que el código ensamblado a mano es mejor que el código compilado, pero generalmente es un ejemplo ficticio o una rutina única, no un verdadero programa de más de 500,000 líneas de código C ++. Creo que los compiladores producirán un mejor código de ensamblado el 95% de veces y , a veces, solo en raras ocasiones, es posible que necesite escribir código de ensamblaje para pocas rutinas de rendimiento crítico breves o muy utilizadas o cuando tenga que acceder a las características de su lenguaje de alto nivel favorito no expone ¿Quieres un toque de esta complejidad? Lea esta increíble respuesta aquí en SO.
¿Por qué esto?
En primer lugar porque los compiladores pueden hacer optimizaciones que ni siquiera podemos imaginar (ver esta breve lista ) y las harán en segundos (cuando podamos necesitar días ).
Cuando codifica en ensamblaje, debe realizar funciones bien definidas con una interfaz de llamada bien definida. Sin embargo, pueden tener en cuenta la optimización de todo el programa y la optimización entre procedimientos , como la asignación de registros , la propagación constante , la eliminación común de subexpresiones , la programación de instrucciones y otras optimizaciones complejas y no obvias ( modelo Polytope , por ejemplo). En la arquitectura RISC , los chicos dejaron de preocuparse por esto hace muchos años (la programación de instrucciones, por ejemplo, es muy difícil de sintonizar a mano ) y las modernas CPU CISC tienen pipelines muy largas.
Para algunos microcontroladores complejos, incluso las bibliotecas del sistema se escriben en C en lugar de ensamblarse porque sus compiladores producen un código final mejor (y fácil de mantener).
Los compiladores a veces pueden usar automáticamente algunas instrucciones de MMX / SIMDx , y si no las usa, simplemente no pueden comparar (otras respuestas ya revisaron su código de ensamblado muy bien). Solo para bucles, esta es una breve lista de optimizaciones de bucle de lo que comúnmente comprueba un compilador (¿crees que podrías hacerlo por ti mismo cuando tu agenda se haya decidido para un programa C #?) Si escribes algo en el ensamblaje, piense que debe considerar al menos algunas optimizaciones simples . El ejemplo de libro de escuela para matrices es para desenrollar el ciclo (su tamaño se conoce en tiempo de compilación). Hazlo y ejecuta tu prueba nuevamente.
En la actualidad, también es poco común tener que usar el lenguaje ensamblador por otra razón: la gran cantidad de CPU diferentes . ¿Quieres apoyarlos a todos? Cada uno tiene una microarchitecture específica y algunos conjuntos de instrucciones específicas . Tienen diferente número de unidades funcionales y las instrucciones de ensamblaje deben organizarse para mantenerlas ocupadas . Si escribe en C, puede usar PGO pero en el ensamblaje necesitará un gran conocimiento de esa arquitectura específica (y repensar y rehacer todo para otra arquitectura ). Para tareas pequeñas, el compilador generalmente lo hace mejor, y para tareas complejas, generalmente el trabajo no se paga (y el compilador puede hacerlo mejor de todos modos).
Si te sientas y echas un vistazo a tu código probablemente verás que ganarás más para rediseñar tu algoritmo que para traducirlo a ensamblaje (lee esta gran publicación aquí en SO ), hay optimizaciones de alto nivel (y sugerencias al compilador) puede aplicar efectivamente antes de que necesite recurrir al lenguaje ensamblador. Probablemente valga la pena mencionar que, a menudo, al utilizar intrínsecos obtendrás una ganancia de rendimiento que estás buscando y el compilador aún podrá realizar la mayoría de sus optimizaciones.
Dicho todo esto, incluso cuando puede producir un código de ensamblaje 5 ~ 10 veces más rápido, debe preguntar a sus clientes si prefieren pagar una semana de su tiempo o comprar una CPU 50 $ más rápida . La mayoría de nosotros simplemente no requiere la optimización extrema más a menudo (y especialmente en aplicaciones LOB).
Su código de ensamblaje es excepcionalmente pobre, ligeramente inferior al óptimo y puede mejorarse:
- Está presionando y mostrando un registro ( EDX ) en su bucle interno. Esto debería moverse fuera del ciclo.
- Vuelve a cargar los punteros de matriz en cada iteración del ciclo. Esto debería moverse fuera del ciclo.
- Utiliza la instrucción de
loop
, que se sabe que es muy lenta en la mayoría de las CPU modernas (posiblemente como resultado de utilizar un antiguo libro de ensamblaje *) - No aprovecha ninguna ventaja del desenrollado manual de bucles.
- No usas instrucciones SIMD disponibles.
Por lo tanto, a menos que mejore ampliamente su conjunto de habilidades con respecto al ensamblador, no tiene sentido que escriba código de ensamblador para el rendimiento.
* Por supuesto, no sé si realmente recibiste la instrucción de loop
de un libro de ensamblaje antiguo. Pero casi nunca lo ves en el código del mundo real, ya que cada compilador es lo suficientemente inteligente como para no emitir loop
, solo lo ves en los libros malos y obsoletos de la IMHO.
es un tema muy interesante!
He cambiado el MMX por SSE en el código de Sasha
Aquí están mis resultados:
Function of C++ version: 315
Function of assembly(simply): 312
Function of assembly (MMX): 136
Function of assembly (SSE): 62
El código de ensamblaje con SSE es 5 veces más rápido que C ++
Simplemente implementando ciegamente el mismo algoritmo, instrucción por instrucción, en ensamble se garantiza que será más lento de lo que el compilador puede hacer.
Es porque incluso la optimización más pequeña que hace el compilador es mejor que su código rígido sin ninguna optimización en absoluto.
Por supuesto, es posible vencer al compilador, especialmente si se trata de una parte pequeña y localizada del código, incluso tuve que hacerlo yo mismo para obtener una aprox. 4 veces más rápido, pero en este caso tenemos que confiar mucho en el buen conocimiento del hardware y en numerosos trucos aparentemente contraintuitivos.
Respuesta corta: sí
Respuesta larga: sí, a menos que realmente sepa lo que está haciendo y tenga una razón para hacerlo.
A c++ compiler would, after optimization at the organizational level, produce code that would utilize the built in functions of the targeted cpu. HLL will never outrun or out-perform assembler for several reasons; 1.) HLL will be compiled and output with Accessor code, boundary checking and possibly built in garbage collection (formerly addressing scope in the OOP mannerism) all requiring cycles (flips and flops). HLL does an excellent job these days (including newer C++ and others like GO), but if they outperform assembler (namely your code) you need to consult the CPU Documentation -comparisons with sloppy code are most certainly inconclusive and compiled langs like assembler all resolve down to op-code HLL abstracts the details and does not eliminate them else you app isn''t going to run if it''s even recognize by the host OS.
Most assembler code (primarily objects) are output as "headless" for inclusion into other executable formats with far less processing required hence it will be much faster, but far more unsecure; if an executable is output by the assembler (NAsm, YAsm; etc.) it will still run faster until it completely matches the HLL code in functionality then results may be accurately weighed.
Calling an assembler based code object from HLL in any format will inherently add processing overhead as well in addition to memory space calls using globally allocated memory for variable/constant data types (this applies to both LLL and HLL). Remember that the final output is using the CPU ultimately as its api and abi relative to the hardware (opcode) and both, assemblers and "HLL compilers" are essentially/fundamentally identical with the only true exception being readability (grammatical).
Hello world console application in assembler using FAsm is 1.5 KB (and this is in Windows even smaller in FreeBSD and Linux) and outperforms anything GCC can throw out on its best day; reasons are implicit padding with nops, access validation and boundary checking to name a few. The real goal is clean HLL libs and an optimizable compiler that targets a cpu in a "hardcore" manner and most do these days (finally). GCC is not better than YAsm -it is the coding practices and understanding of the developer that are in question and "optimization" comes after novice exploration and interim training & experience.
Compilers have to link and assemble for output in the same opcode as an assembler because those codes are all that a CPU will except (CISC or RISC [PIC too]). YAsm optimized and cleaned up a great deal on early NAsm ultimately speeding up all output from that assembler, but even then YAsm still, like NAsm, produce executables with external dependencies targeting OS libraries on behalf of the developer so mileage may vary. In closing C++ is at a point that is incredible and far more safe than assembler for 80+ percent especially in the commercial sector...
All the answers here seem to exclude one aspect: sometimes we don''t write code to achieve a specific aim, but for the sheer fun of it. It may not be economical to invest the time to do so, but arguably there is no greater satisfaction than beating the fastest compiler optimized code snippet in speed with a manually rolled asm alternative.
Assembly could be faster if your compiler generates a lot of OO support code.
Editar:
To downvoters: the OP wrote "should I ... focus on C++ and forget about assembly language?" and I stand by my answer. You always need to keep an eye on the code OO generates, particularly when using methods. Not forgetting about assembly language means that you will periodically review the assembly your OO code generates which I believe is a must for writing well-performing software.
Actually, this pertains to all compileable code, not just OO.
In recent times, all the speed optimisations that I have done were replacing brain damaged slow code with just reasonable code. But for things were speed was really critical and I put serious effort into making something fast, the result was always an iterative process, where each iteration gave more insight into the problem, finding ways how to solve the problem with fewer operations. The final speed always depended on how much insight I got into the problem. If at any stage I used assembly code, or C code that was over-optimised, the process of finding a better solution would have suffered and the end result would be slower.