example - Ist die Inline-Assemblersprache langsamer als nativer C++-Code?



performance assembly (15)

Heißt das, ich sollte der Ausführung der von meinen Händen geschriebenen Assemblersprache nicht vertrauen

Ja, genau das ist es, und es gilt für jede Sprache. Wenn Sie nicht wissen, wie man effizienten Code in Sprache X schreibt, dann sollten Sie Ihrer Fähigkeit, effizienten Code in X zu schreiben, nicht vertrauen. Wenn Sie also effizienten Code wollen, sollten Sie eine andere Sprache verwenden.

Die Versammlung ist besonders empfindlich dafür, denn, was Sie sehen, ist was Sie bekommen. Sie schreiben die spezifischen Anweisungen, die die CPU ausführen soll. Bei Hochsprachen gibt es einen Compiler, der Ihren Code transformieren und viele Ineffizienzen beseitigen kann. Mit der Montage bist du alleine.

Ich habe versucht, die Leistung der Inline-Assemblersprache und des C ++ - Codes zu vergleichen, also schrieb ich eine Funktion, die 100000 Mal zwei Arrays der Größe 2000 hinzufügt. Hier ist der Code:

#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
    };
}

Hier ist 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;
}

Dann führe ich das Programm fünfmal aus, um die Zyklen des Prozessors zu erhalten, die als Zeit angesehen werden könnten. Jedes Mal rufe ich eine der oben genannten Funktionen auf.

Und hier kommt das Ergebnis.

Funktion der Montageversion:

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

Funktion der C ++ Version:

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

Der C ++ - Code im Freigabemodus ist fast 3,7 mal schneller als der Assemblercode. Warum?

Ich nehme an, dass der Assemblercode, den ich geschrieben habe, nicht so effektiv ist wie der von GCC erzeugte. Es ist schwer für einen gewöhnlichen Programmierer wie mich, Code schneller zu schreiben als sein Gegner, der von einem Compiler generiert wurde. Bedeutet das, dass ich der Leistung der von meinen Händen geschriebenen Assemblersprache nicht vertrauen sollte, sich auf C ++ konzentrieren und die Assemblersprache vergessen sollte?


Answer #1

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.


Answer #2

Der Compiler hat dich geschlagen. Ich werde es versuchen, aber ich werde keine Garantien geben. Ich gehe davon aus, dass die "Multiplikation" durch TIMES einen relevanteren Leistungstest machen soll, dass y und x 16-ausgerichtet sind und dass die length ein Vielfaches von 4 ist. Das ist wahrscheinlich alles wahr.

  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

Wie gesagt, ich mache keine Garantien. Aber ich bin überrascht, wenn es viel schneller geht - der Flaschenhals hier ist Speicherdurchsatz, auch wenn alles ein L1-Hit ist.


Answer #3

Der einzige Grund, Assemblersprache heutzutage zu verwenden, besteht darin, einige Funktionen zu verwenden, auf die die Sprache nicht zugreifen kann.

Dies gilt für:

  • Kernel-Programmierung, die auf bestimmte Hardware-Features wie die MMU zugreifen muss
  • Hochleistungs-Programmierung, die sehr spezifische Vektor- oder Multimedia-Anweisungen verwendet, die von Ihrem Compiler nicht unterstützt werden.

Aber aktuelle Compiler sind ziemlich schlau, sie können sogar zwei separate Anweisungen wie d = a / b; r = a % b; ersetzen d = a / b; r = a % b; d = a / b; r = a % b; mit einer einzigen Anweisung, die die Division und den Rest auf einmal berechnet, wenn sie verfügbar ist, auch wenn C keinen solchen Operator hat.


Answer #4

Es ist ein sehr interessantes Thema!
Ich habe das MMX von SSE in Sasha's Code geändert
Hier sind meine Ergebnisse:

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

Der Assembly-Code mit SSE ist 5 mal schneller als das C ++


Answer #5

Es ist genau das, was es bedeutet. Überlassen Sie die Mikrooptimierungen dem Compiler.


Answer #6

Ich habe den asm-Code geändert:

 __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
};

Ergebnisse für die Release-Version:

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

Der Assembly-Code im Freigabemodus ist fast 4-mal schneller als das C ++. IMHo, die Geschwindigkeit des Assemblercodes hängt vom Programmierer ab


Answer #7

Ich habe meinen ASM-Code korrigiert:

  __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
};

Ergebnisse für die Release-Version:

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

Der Assemblycode im Freigabemodus ist fast 2 mal schneller als C ++.


Answer #8

Ihr Assembler-Code ist außergewöhnlich schlecht, leicht suboptimal und kann verbessert werden:

  • Sie schieben und knallen ein Register ( EDX ) in Ihrer inneren Schleife. Dies sollte aus der Schleife entfernt werden.
  • Sie laden die Array-Zeiger in jeder Iteration der Schleife neu. Dies sollte aus der Schleife entfernt werden.
  • Sie verwenden den loop , von dem bekannt ist, dass er bei den meisten modernen CPUs langsam ist (möglicherweise aufgrund eines alten Assemblerbuchs *).
  • Sie profitieren nicht vom manuellen Schleifenabwickeln.
  • Sie verwenden keine verfügbaren SIMD Anweisungen.

Wenn Sie also Ihre Fähigkeiten in Bezug auf Assembler nicht wesentlich verbessern, macht es keinen Sinn, Assembler-Code für die Performance zu schreiben.

* Natürlich weiß ich nicht, ob du wirklich die loop von einem alten Versammlungsbuch bekommen hast. Aber man sieht es fast nie im Code der realen Welt, da jeder Compiler dort schlau genug ist, keine loop auszugeben, man sieht es nur in schlechten und veralteten Büchern.


Answer #9

In vielen Fällen kann der optimale Weg zur Ausführung einer Aufgabe von dem Kontext abhängen, in dem die Aufgabe ausgeführt wird. Wenn eine Routine in Assemblersprache geschrieben wird, ist es im Allgemeinen nicht möglich, dass die Sequenz von Anweisungen basierend auf dem Kontext variiert wird. Betrachten Sie als einfaches Beispiel die folgende einfache Methode:

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

Ein Compiler für 32-Bit-ARM-Code, der dem oben genannten entspricht, würde es wahrscheinlich als etwas wie Folgendes rendern:

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

oder vielleicht

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]

Das könnte leicht in hand-assembliertem Code optimiert werden, entweder:

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

oder

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

Beide von Hand zusammengestellten Ansätze würden 12 Byte Coderaum anstelle von 16 benötigen; Letzteres würde ein "Laden" durch ein "Hinzufügen" ersetzen, was auf einem ARM7-TDMI zwei Zyklen schneller ausführen würde. Wenn der Code in einem Kontext ausgeführt werden würde, in dem r0 nicht weiß / nicht interessiert ist, wären die Assemblerversionen etwas besser als die kompilierte Version. Nehmen wir andererseits an, der Compiler wisse, dass ein Register [z. B. r5] einen Wert halten würde, der innerhalb von 2047 Bytes der gewünschten Adresse 0x40001204 [z. B. 0x40001000] lag, und wusste, dass ein anderes Register [z um einen Wert zu halten, dessen niedrige Bits 0xFF waren. In diesem Fall könnte ein Compiler die C-Version des Codes optimieren, um einfach:

strb r7,[r5+0x204]

Viel kürzer und schneller als selbst der handoptimierte Assemblercode. Angenommen, set_port_high ist im Kontext aufgetreten:

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

Beim Kodieren für ein eingebettetes System gar nicht plausibel. Wenn set_port_high in Assembler-Code geschrieben wird, müsste der Compiler r0 (der den Rückgabewert von function1 ) vor dem Aufruf des Assembly-Codes an eine andere Stelle verschieben und diesen Wert danach wieder auf r0 verschieben (da function2 seinen ersten Parameter erwartet r0), so dass der "optimierte" Assemblercode fünf Anweisungen benötigt. Selbst wenn der Compiler keine Register kannte, die die Adresse oder den zu speichernden Wert enthielten, würde seine Version mit vier Befehlen (die er anpassen könnte, um irgendwelche verfügbaren Register zu verwenden - nicht notwendigerweise r0 und r1), die "optimierte" Assembly schlagen -langes Version. Wenn der Compiler die erforderliche Adresse und Daten in r5 und r7 hätte, wie zuvor beschrieben, würde function1 diese Register nicht ändern und könnte daher set_port_high durch einen einzigen strb Befehl ersetzen set_port_high vier Befehle, die kleiner und schneller als die " strb " Assembly sind Code.

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.


Answer #10

Noch bevor man sich der Assembly widmet, gibt es Code-Transformationen, die auf einer höheren Ebene existieren.

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];
    }
  }
}

kann über Loop Rotation umgewandelt werden :

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];
      }
    }
}

was viel besser ist, was die Speicherlokalität angeht.

Dies könnte weiter optimiert werden, indem man a += b X mal macht, ist das äquivalent zu a += X * b also erhalten wir:

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];
    }
}

Es scheint jedoch, dass mein Lieblingsoptimierer (LLVM) diese Transformation nicht durchführt.

[edit] Ich fand, dass die Umwandlung durchgeführt wird, wenn wir das restrict Qualifikationsmerkmal zu x und y . In der Tat könnten x[j] und y[j] ohne diese Einschränkung zu derselben Stelle führen, wodurch diese Transformation fehlerhaft wird. [Ende bearbeiten]

Wie auch immer, das ist, denke ich, die optimierte C-Version. Schon ist es viel einfacher. Basierend darauf, hier ist mein Riss bei ASM (ich ließ Clang es erzeugen, ich bin nutzlos darin):

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

Ich fürchte, ich verstehe nicht, woher all diese Anweisungen kommen, aber Sie können immer Spaß haben und versuchen zu sehen, wie es sich vergleicht ... aber ich würde immer noch die optimierte C-Version anstelle der Assembly verwenden, im Code, viel tragbarer.


Answer #11

Die bloße Implementierung des gleichen Algorithmus, Anweisung für Anweisung, in Assembly ist garantiert langsamer als das, was der Compiler tun kann.

Das liegt daran, dass selbst die kleinste Optimierung des Compilers besser ist als Ihr rigider Code ohne Optimierung.

Natürlich ist es möglich, den Compiler zu besiegen, besonders wenn es sich um einen kleinen, lokalisierten Teil des Codes handelt, ich musste es sogar selbst machen, um einen ungefähren Code zu bekommen. 4x schneller, aber in diesem Fall müssen wir uns auf gute Kenntnisse der Hardware und zahlreicher, scheinbar kontraintuitiver Tricks verlassen.


Answer #12

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...


Answer #13

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.


Answer #14

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.





assembly