Win(BCB)C++File mit Assemblercode in QT/Unix Projekt einbauen



  • Hallo
    ich fang erst mit QT an und möchte mein Windos Programm(Build 2008) mit QT auf LInux umbauen. Da ein C++ File viele Funktionen in Assembler hat, bekomme ich es auf QT mit GCC nicht zum Compilieren. Wie oder was muß in CMake oder im Kit ändern? Das, was ich vom Inline-ass gelesen habe kann ich offensichtlich nicht brauchen.

    void bgi::addieren(unsigned *a,unsigned *b,unsigned *z)
    {
    
    _asm {
    		xor		eax, eax				// CF = 0 !!
    		mov		esi, a
    		mov		edi, b
    		mov		ebx, z
    		movsx	ecx, [ebx].Laenge  	        	// Z�hler init
    adduuu1:mov		eax, esi[ecx*4-4]
    		adc		eax, edi[ecx*4-4]
    		mov		ebx[ecx*4-4], eax
    		loop	adduuu1
    		}
    }
    


  • @RudiMBM
    Oh je, ich fürchte du musst den ganzen Assembler-Code überarbeiten, da sich die GNU Assembler Syntax etwas von der Intel Syntax unterscheidet.

    https://de.wikipedia.org/wiki/GNU_Assembler

    Evt. hilft hier die .intel_syntax noprefix Anweisung.

    BTW: Gibt es eine Möglichkeit den Code in C++ zu schreiben?



  • @Quiche-Lorraine sagte in [Win(BCB)C++File mit Assemblercode in QT/Unix Projekt einbauen](/forum

    BTW: Gibt es eine Möglichkeit den Code in C++ zu schreiben?

    @RudiMBM Das würde ich auch empfehlen, allerdings verstehe ich, weshalb das in Assembler implementiert wurde: Wenn ich das richtig lese, handelt es sich um eine Big-integer-Addition(?), welche sich die ADC-Instruktion der x86-CPUs zunutze macht. Das ist eine Addition mit Übertrag und hat keine direkte Entsprechung in C++. Man ist also darauf angewiesen, dass der Compiler das gut optimiert.

    Wenn er das ADC hinbekommt, dann wäre eine C++-Version meiner Meinung nach zu bevorzugen. Der Assembler-Code wird nämlich meines Wissens so übernommen wie er da steht, während mit C++ Code der Compiler z.B. noch die Schleife vektorisieren und den Code möglicherweise besser inlinen könnte.

    Das müsste man allerdings mal austesten und sich genau ansehen, was für Code der Compiler erzeugt - und wenn die Performance wichtig ist, natürlich immer messen. Wenn ich später Zeit dazu finde, dann spiele ich damit mal etwas herum, das interessiert mich auch, was der Compiler aus einer C++-Implementierung macht.

    Alternativ kann man das natürlich in GCC-kompatibles Inline-Assembler übertragen. Das ist ein bisschen komplizierter als die MSVC-_asm-Blöcke, da sollstest du dich vielleicht mal etwas einlesen (ich mach sowas auch nur selten und müsste das auch wieder tun).


  • Mod

    Kann man den GCC nicht sogar insgesamt auf Intelsyntax umstellen mit dem -masm Parameter? Dann braucht man nicht jedes Stück Assembler mit .intel_syntax noprefix zu versehen. Geht natürlich nicht, wenn irgendwo im Programm ein anderer Stil gemischt wird, aber das ist ja eher unwahrscheinlich. Habe ich aber noch nie benutzt, ich garantiere für nix.

    https://gcc.gnu.org/onlinedocs/gcc/x86-Options.html



  • @SeppJ sagte in Win(BCB)C++File mit Assemblercode in QT/Unix Projekt einbauen:

    Dann braucht man nicht jedes Stück Assembler mit .intel_syntax noprefix zu versehen. Geht natürlich nicht, wenn irgendwo im Programm ein anderer Stil gemischt wird, aber das ist ja eher unwahrscheinlich. Habe ich aber noch nie benutzt, ich garantiere für nix.

    Es reicht einmal am Anfang des Inline-Assembler-Blocks und das würde ich auch gerade wegen dem "gemischten Stil" empfehlen (und weil diese gruselige AT&T-Sytax irgendwie Standard in der GCC-Welt ist). Dass sich der Code ohne eine eine spezielle Compiler-Option bauen lässt, erachte ich als Vorteil, der den Preis der extra Zeilen wert ist IMHO.



  • Danke an Alle. @Finnegan du hast es gleich erkannt. Das ist aber eine Ganze Bigint-Lib, allerdings nur Grundarten und spezielles und soll außerdem schnell sein. @Quiche-Lorraine, der Wikilink ist gut. Obwohl ich selbst auf Wiki helfe, habe ich nur nach dem GCC geschaut, aber da ist kein Link auf den Assembler. Den werde ich gleich einbauen.
    Ich habe mir auch mit einem anderen Linux-Tool eine .obj Datei machen kann, die ich QT verwenden kann. Kenn ich aber nix.



  • Alternativ könnte man prüfen ob man nicht eine externe bigint library verwendet, statt eine eigene Implementierung.
    Besonders, wenn man neben verschiedenen Betriebsystemen auch noch andere CPU architekturn (z.b. ARM) in zukunft unterstützen möchte.

    Zwei Beispiele die ich mit einer kurzen suche gefunden habe:
    https://gmplib.org/
    https://www.boost.org/doc/libs/1_84_0/libs/multiprecision/doc/html/index.html (Was wohl intern gmplib nutzt)



  • @RudiMBM Ich habe nochmal was rumexperimentiert:

    #include <cstdint>
    #include <cassert>
    
    void add1(
        const std::uint32_t* a,
        const std::uint32_t* b,
        std::uint32_t* result,
        unsigned length
    )
    {
        assert(length > 0);
        std::uint32_t carry = 0;
        for (unsigned i = length - 1; i >= 0; --i)
        {
            std::uint32_t value = a[i];
            value += b[i] + carry;
            carry = value < a[i];
            result[i] = value;
        }
    }
    
    void add2(
        const std::uint32_t* a,
        const std::uint32_t* b,
        std::uint32_t* result,
        unsigned length
    )
    {
        assert(length > 0);
        std::uint32_t carry = 0;
        for (unsigned i = length - 1; i >= 0; --i)
            result[i] = __builtin_addc(a[i], b[i], carry, &carry);
    }
    
    void add3(
        const std::uint32_t* a,
        const std::uint32_t* b,
        std::uint32_t* result,
        unsigned length
    )
    {
        assert(length > 0);
        asm volatile
        (
            R"(
                .intel_syntax noprefix
                xor		eax, eax
            1:
                mov		eax, dword ptr [esi + ecx * 4 - 4]
                adc		eax, dword ptr [edi + ecx * 4 - 4]
                mov		dword ptr [ebx + ecx * 4 - 4], eax
                loop	1 b
            )"
            :
            : "S" (a), "D" (b), "b" (result), "c" (length)
            : "memory", "cc"
        );
    }
    
    void add4(
        const std::uint32_t* a,
        const std::uint32_t* b,
        std::uint32_t* result,
        unsigned length
    )
    {
        assert(length > 0);
        asm volatile
        (
            R"(
                .intel_syntax noprefix
                xor		eax, eax
            1:
                mov		eax, dword ptr [edi + ecx * 4 - 4]
                adc		eax, dword ptr [esi + ecx * 4 - 4]
                mov		dword ptr [edx + ecx * 4 - 4], eax
                loop	1 b
            )"
            :
            : "D" (a), "S" (b), "d" (result), "c" (length)
            : "memory", "cc"
        );
    }
    

    GCC: https://godbolt.org/z/j17recbq5
    Clang: https://godbolt.org/z/3nvaPdn6o

    add1 ist eine naive C++-Implementierung. Nach meinem Verständnis sollte eigentlich der Ausdruck value < a[i] nach der Addition genau dann true sein, wenn ein Überlauf stattgefunden hat, also dem Carry-Flag entsprechen. GCC und Clang scheinen das allerdings nicht zu erkennen, bzw. aus anderen Gründen hier keine ADC-Instruktion zu generieren. Das heisst aber nicht zwangsläufig, dass das schlechter Assembler-Code ist, das weiss man erst wenn man das gegen die Alternativen benchmarkt.

    add2 verwendet das GCC/Clang-spezifische Builtin __builtin_addc das soweit ich das sehe der ADC-Instruktion entsprechen sollte. Hier generiert GCC dann auch eine ADC-Instruktion, Clang scheint das aber nicht für notwendig (oder keine gute Idee) zu halten. So ein Builtin hat natürlich genau wie die C++-Implementierung den Vorteil, dass es nicht CPU-spezifisch ist. Das wird auch auf ARM und anderen CPUs funktionieren, die entweder kein ADC kennen oder wo solche Additionen mit Übertrag anders gemacht werden können.

    add3 ist die GCC/Clang-spezifische Inline-Assember-Implementierung. volatile hat hier den Effekt, dass die Assembler-Instruktionen "wie sie sind" übernommen und nicht vom Compiler weiter optimiert/umgeschrieben werden sollen. Die Inputs sind hier wie folgt definiert (der Compiler initialisiert die Register entsprechend):

    "S" (a): Wert (Adresse des Pointers) von a in ESI (bzw. RSI für 64-Bit Code)
    "D" (b): Wert (Adresse des Pointers) von b in EDI (bzw. RDI für 64-Bit Code)
    "b" (result): Wert (Adresse des Pointers) von result in EBX (bzw. RBX für 64-Bit Code)
    "c" (length): Wert von length in ECX (bzw. RCX für 64-Bit Code)

    : "memory", "cc" ist die Clobber-Liste, damit teilt man dem Compiler mit, was der Assembler-Code alles modifiziert. Das ist notwendig, damit der Compiler korrekten Code erzeugt (z.B. muss der Compiler eventuell ein Register sichern oder Daten aus dem Speicher neu laden, wenn die vom ASM-Code verändert wurden): memory bedeutet, dass der Code Speicher veändert, nämlich den, auf den result zeigt. cc bedeutet, dass das Flags-Register verändert wird (wegen der Addition). EAX, sowie Register, die in der Input-Liste initialisiert werden, müssen hier nicht nochmal extra angegegeben werden.

    Ich sollte auch nochmal darauf Hinweisen, dass dieser Code genau wie deiner auch 32-Bit-spezifisch ist (!), da für die Speicheradressen die 32-Bit-Register ESI, EDI und EBX verwendet werden. Das mag zwar auf den ersten Blick auch mit 64-Bit funktionieren, aber nur dann, wenn die Speicheradressen unterhalb der 4GiB-Grenze liegen. Darüber würde der Code wahrscheinlich vor die Wand laufen, da die oberen 32 Bit der Adresse hier einfach abgeschnitten werden. Die 64-Bit-Varianten dieser Register wären RSI, RDI und RBX. Das müsste man im Code noch entsprechen handhaben, wenn man mit 64-Bit-Adressen arbeitet, bzw. eine Compiler-Weiche einbauen, wenn man auch noch 32-Bit unterstützen will. Das macht es alles nicht unkomplizierter und ist noch ein Grund mehr für eine nicht-ASM-Implementierung (C++ oder Builtin) oder eben eine andere BigInt-Bibliothek zu verwenden, welche diese Probleme alle für dich löst.

    add4 ist im Prinzip dasselbe wie add3 allerdings habe ich hier den Assembler-Code so umgeschieben, dass er die Register verwendet, in denen die Funktionsargumente per x86 Calling Convention reinkommen. Das macht den Assembler-Code noch etwas kürzer, da die Register dann nicht mehr so umgetauscht werden müssen, dass sie auf den Code passen - sondern der Code passt direkt auf die Register.

    Einen Benchmark habe ich nicht gemacht und auch wenn der erzeugte Code von add4 auf den ersten Blick sehr attraktiv kompakt aussieht, weiss man erst ob der wirklich was taugt, wenn man die Varianten gemessen hat.

    Auch sollte ich noch unterstreichen, dass ich meinen ganzen Code hier nicht auf Korrektheit geprüft habe. Es ist also gut möglich, dass hier irgendwo Blödsinn berechnet wird. Er ist aber nach "bestem Wissen und Gewissen runtergeschrieben" falls das was Wert ist 😁 .

    Und nochwas: Bis auf add1 kompilieren diese Lösungen natürlich alle nur mit GCC oder Clang und add3/add4 sind auch noch x86 only, sogar nur 32-Bit (s.o.). Also ordentlich Compiler-Weichen verwenden und alternative Implementierungen bereitstellen (z.B. deine MSVC-ASM-Lösung, wenn mit MSVC gebaut wird oder eine C++-Implementierung wenn z.B. auch mit MSVC für ARM kompiliert werden soll).



  • @Finnegan super, mensch da dast du dir aber arbeit gemacht. Da ich viele Ass-Teile habe ist es mir zu aufwendig. Ursprünglich wollte ich mal was schnelles haben und LIb mit variablen Zifferanzahlen, aber zugunsten der Handelbarkeit kann auf schnell verzichten. Wobei ich auch keinen Speedtest mit einer externen bigint library gemacht habe. Ich muß nur noch rausfinden, ob eine solche auch dynamische Variablenlängen zulässt. Außerdem könne ich mit QT/GCC dann auch ausser Linux/X64 evtl. noch andere Systeme zu compilien. (Falls ich überhaupt so weit komme. Das BCB/2008 auf Win7 Projekt war mein letztes größeres. Es hat sich in 15 Jahren soviel verändert, dass mir scheint, ich muß trotz 30Jahren Computerei mal wieder neu anfangen).

    @Finnegan kann es sein, dass du spass am Programmieren hast? Vielleich auch bei meinen Projekt mitmachen? Kannst mir dann mal Mailen.


Log in to reply