Warum ist memcmp(a, b, 4) nur manchmal für einen uint32-Vergleich optimiert?

Lesezeit: 5 Minuten

Benutzeravatar von John Zwinck
Johannes Zwinck

Angesichts dieses Codes:

#include <string.h>

int equal4(const char* a, const char* b)
{
    return memcmp(a, b, 4) == 0;
}

int less4(const char* a, const char* b)
{
    return memcmp(a, b, 4) < 0;
}

GCC 7 auf x86_64 führte eine Optimierung für den ersten Fall ein (Clang macht das schon lange):

    mov     eax, DWORD PTR [rsi]
    cmp     DWORD PTR [rdi], eax
    sete    al
    movzx   eax, al

Aber der zweite Fall ruft immer noch memcmp():

    sub     rsp, 8
    mov     edx, 4
    call    memcmp
    add     rsp, 8
    shr     eax, 31

Könnte eine ähnliche Optimierung auf den zweiten Fall angewendet werden? Was ist die beste Montage dafür, und gibt es einen klaren Grund, warum dies nicht getan wird (von GCC oder Clang)?

Sehen Sie es im Compiler Explorer von Godbolt: https://godbolt.org/g/jv8fcf

  • Was ich interessant finde, ist die beiläufige Missachtung der Ausrichtung; Dies funktioniert möglicherweise mit x86, aber auf anderen CPUs ist die Optimierung möglicherweise nicht gültig.

    – Matthias M.

    12. Juli 2017 um 15:02 Uhr

  • @MatthieuM. es muss nur auf der Zielarchitektur gültig sein

    – Caleth

    12. Juli 2017 um 15:21 Uhr

  • @Caleth: Stimme zu, aber ich frage mich, in welchem ​​​​Stadium die Transformation durchgeführt wird. Das heißt, ob gcc in seinem mittleren Ende eine zielspezifische Optimierung verwendet (vielleicht abstrahiert) oder ob es Teil der Absenkung ist.

    – Matthias M.

    12. Juli 2017 um 16:31 Uhr

  • @MatthieuM. Sie können es herausfinden, indem Sie mit kompilieren -fdump-tree-all -fdump-rtl-all (zusätzlich zu allen anderen Schaltern). Dadurch wird die Zwischendarstellung nach jeder Optimierungsphase in eine Datei im aktuellen Arbeitsverzeichnis abgelegt, die nummeriert ist, damit Sie sie der Reihe nach durchlesen können. (Sie erhalten ungefähr 300 Dateien, wenn Sie dies tun. Die “Baum”-Dumps sind viel einfacher zu lesen als die “RTL”-Dumps. Sie sollten wahrscheinlich die Kapitel “RTL” und “Maschinenbeschreibung” der Internes Handbuch bevor Sie versuchen, die RTL-Dumps zu lesen.)

    – zol

    12. Juli 2017 um 18:05 Uhr

  • Da ist ein bswap Anweisung in x86 und ARM hat rev. Eine zusätzliche Anweisung, obwohl.

    – MSalter

    12. Juli 2017 um 11:12 Uhr

  • @CodyGray: Wie dasblinkenlight betont, reicht das aus, um es zu sagen <0 und >0 ein Teil. Arithmetisch sucht CMP nach dem signifikantesten Bitunterschied, während memcmp sucht nach dem ersten abweichenden Byte in der Speicherreihenfolge. Auf Big-Endian-Systemen enthält das erste Byte das MSB. bswap wandelt das native Little-Endian-Bitmuster in Big-Endian um, warum?

    – MSalter

    12. Juli 2017 um 13:14 Uhr

  • @Kevin: Sie möchten sowieso nicht die Bytes im Speicher austauschen (und sie dann wiederherstellen)! Der optimale Asm könnte etwa so aussehen: Lade beide 4B-Blöcke in Register, vertausche sie beide per Byte. Es sind also zusätzliche Anweisungen für die Decoder und zusätzliche Fused-Domain-Uops für das Front-End, die ausgegeben werden müssen, da Sie keinen Speicheroperanden verwenden können cmp wie für die == 0 Fall. z.B mov edi, [rdi] / mov esi, [rsi] / bswap edi /bswap esi / cmp edi, esi / seta und movzx. Das sind alles Single-Uop-Anweisungen auf allen neueren Intel- und AMD-CPUs (agner.org/optimieren)

    – Peter Cordes

    12. Juli 2017 um 17:40 Uhr


  • @Kevin und const-correct gelten nur für die Quelle. Die CPU kann tun, was sie will, solange das Endergebnis dasselbe ist.

    – Orangenhund

    12. Juli 2017 um 18:11 Uhr

  • @OrangeDog – nicht wirklich. Wenn Argumente deklariert werden const char * Sie könnten sehr gut als definiert worden sein const was bedeutet, dass sie schreibgeschützt sein könnten und der Versuch, sie zu ändern, einen Fehler verursachen würde. In der realen Welt passiert genau das bei deklarierten Dingen const char *: Sie werden in der platziert .rodata Segment, das ohne Schreibrechte geladen wird. Die Arbeit auf der asm-Ebene trägt nicht dazu bei, dies zu mildern.

    – BeeOnRope

    13. Juli 2017 um 0:31 Uhr

  • Das stimmt, aber die Frage war, ob das wegoptimiert werden sollte memcmp() Anruf. Es könnte immer noch getan werden, indem man Byte-Swap-Anweisungen aussendet, bevor man den Vergleich durchführt, richtig?

    – Johannes Zwinck

    12. Juli 2017 um 12:36 Uhr

  • @JohnZwinck Ich denke, das Byte-Swapping dafür wäre die Behandlung von a sehr besonders Fall, mit dem sich die Compiler-Autoren nicht beschäftigen.

    – Ruslan

    12. Juli 2017 um 13:07 Uhr

  • @Ruslan: Compiler sind voll von solchen kleinen Optimierungen; Ich bin mir ziemlich sicher, dass die Compiler-Entwickler sich über einen Patch freuen würden, der dies abdeckt … wenn es wirklich funktioniert.

    – Matthias M.

    12. Juli 2017 um 15:01 Uhr

  • @MatthieuM.: Wenn dies in echtem Code wichtig ist, erhalten Sie mit aktuellen Compilern bessere Ergebnisse endian.h oder ähnliche Byte-Swap-Funktionen, um zwei zu erhalten uint32_t Ganzzahlen zu vergleichen. Siehe meine Antwort für ein Beispiel. Aber dann müssen Sie sich Gedanken über falsch ausgerichtete Lasten und so weiter machen, wenn Sie portablen Code schreiben, also wäre es schön, wenn Sie Big-Endian-Ganzzahlen mit unbekannter Ausrichtung vergleichen könnten memcmp und optimalen Code erhalten.

    – Peter Cordes

    12. Juli 2017 um 20:08 Uhr


  • Ich habe mich ein wenig mit dem GCC-Quellcode beschäftigt. Diese Optimierung wird von implementiert handle_builtin_memcmp in der etwas ungenau benannten tree-ssa-strlen.c und wenn ich es richtig lese, implementiert es nur die == und != Fällen: Die Überprüfungen in den Zeilen 2102-3 und 2108-9 bewirken, dass es aussteigt, ohne etwas zu tun, wenn der Vergleich dies nicht tut EQ_EXPR oder NE_EXPR, was bedeutet, wie sie klingen. Später springt es auch aus, wenn !SLOW_UNALIGNED_ACCESS (mode, align) was bedeutet “Können wir diese Ladung durchführen, ohne uns Gedanken über die Ausrichtung zu machen?”

    – zol

    12. Juli 2017 um 21:49 Uhr

  • @zwol: danke! Ich bin nicht überrascht, dass die erste Implementierung dieser neuen Funktion nur behandelt == / != Vergleiche. Schade, dass es keine gibt wirklich portable Endian-Funktionen, die das Schreiben der less4 auf eine Compiler-freundliche Weise ohne ein Durcheinander von #ifdefs.

    – Peter Cordes

    12. Juli 2017 um 21:54 Uhr

  • Die Aliasing-Regeln sind übrigens asymmetrisch: char * kann alles aliasieren, aber int * offiziell kippen alias charzumindest wenn die char als solche deklariert wird; siehe stackoverflow.com/questions/30967447/…

    – zol

    12. Juli 2017 um 23:29 Uhr

  • FWIW, die portable Snippets-Bibliothek, hat eine endian Modul, das dies “schnell” (wie auch der Rest der Bibliothek) angemessen zu erledigen scheint und von hoher Qualität zu sein scheint und aktiv gewartet wird.

    – BeeOnRope

    13. Juli 2017 um 17:59 Uhr


1416010cookie-checkWarum ist memcmp(a, b, 4) nur manchmal für einen uint32-Vergleich optimiert?

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy