Warum ist “signed int” in C schneller als “unsigned int”?

Lesezeit: 9 Minuten

Benutzeravatar von Devyn Collier Johnson
DevynCollier Johnson

In C ist warum signed int schneller als unsigned int? Stimmt, ich weiß, dass dies auf dieser Website mehrfach gefragt und beantwortet wurde (Links unten). Die meisten Leute sagten jedoch, dass es keinen Unterschied gibt. Ich habe Code geschrieben und versehentlich einen signifikanten Leistungsunterschied festgestellt.

Warum sollte die „unsignierte“ Version meines Codes langsamer sein als die „signierte“ Version (auch wenn dieselbe Nummer getestet wird)? (Ich habe einen x86-64 Intel-Prozessor).

Ähnliche Verbindungen

  • Vergleicht schneller vorzeichenbehaftete als vorzeichenlose Ints
  • Leistung von vorzeichenlosen vs. vorzeichenbehafteten Ganzzahlen

Befehl kompilieren: gcc -Wall -Wextra -pedantic -O3 -Wl,-O3 -g0 -ggdb0 -s -fwhole-program -funroll-loops -pthread -pipe -ffunction-sections -fdata-sections -std=c11 -o ./test ./test.c && strip --strip-all --strip-unneeded --remove-section=.note --remove-section=.comment ./test


signed int Ausführung

HINWEIS: Es gibt keinen Unterschied, wenn ich dies ausdrücklich erkläre signed int auf allen Nummern.

int isprime(int num) {
    // Test if a signed int is prime
    int i;
    if (num % 2 == 0 || num % 3 == 0)
        return 0;
    else if (num % 5 == 0 || num % 7 == 0)
        return 0;
    else {
        for (i = 11; i < num; i += 2) {
            if (num % i == 0) {
                if (i != num)
                    return 0;
                else
                    return 1;
            }
        }
    }
    return 1;
}

unsigned int Ausführung

int isunsignedprime(unsigned int num) {
    // Test if an unsigned int is prime
    unsigned int i;
    if (num % (unsigned int)2 == (unsigned int)0 || num % (unsigned int)3 == (unsigned int)0)
        return 0;
    else if (num % (unsigned int)5 == (unsigned int)0 || num % (unsigned int)7 == (unsigned int)0)
        return 0;
    else {
        for (i = (unsigned int)11; i < num; i += (unsigned int)2) {
            if (num % i == (unsigned int)0) {
                if (i != num)
                    return 0;
                else
                    return 1;
            }
        }
    }
    return 1;
}

Testen Sie dies in einer Datei mit dem folgenden Code:

int main(void) {
    printf("%d\n", isprime(294967291));
    printf("%d\n", isprime(294367293));
    printf("%d\n", isprime(294967293));
    printf("%d\n", isprime(294967241)); // slow
    printf("%d\n", isprime(294967251));
    printf("%d\n", isprime(294965291));
    printf("%d\n", isprime(294966291));
    printf("%d\n", isprime(294963293));
    printf("%d\n", isprime(294927293));
    printf("%d\n", isprime(294961293));
    printf("%d\n", isprime(294917293));
    printf("%d\n", isprime(294167293));
    printf("%d\n", isprime(294267293));
    printf("%d\n", isprime(294367293)); // slow
    printf("%d\n", isprime(294467293));
    return 0;
}

Ergebnisse (time ./test):

Signed - real 0m0.949s
Unsigned - real 0m1.174s

  • Es kann nur am Overhead des gesamten expliziten Castings liegen.

    – Cᴏʀʏ

    8. Dezember 2015 um 20:13 Uhr


  • Als erstes würde ich den generierten Assembler-Code für beide Fälle vergleichen und prüfen, ob im unsignierten Fall zusätzliche Anweisungen ausgegeben werden.

    – Tom Karzes

    8. Dezember 2015 um 20:19 Uhr

  • Beim Schreiben von Benchmarks empfehle ich dringend, die Messzeit auf mindestens 30 Sekunden zu erhöhen und den Task, der den Benchmark ausführt, mit der höchstmöglichen Priorität auszuführen. Andernfalls kann Ihr gemessener Unterschied von 20 % stark vom Betriebssystem verursacht werden.

    – Lukas Thomsen

    8. Dezember 2015 um 20:22 Uhr

  • @Cᴏʀʏ sie sind sowieso freie Besetzungen. Sie weisen den Compiler an, den Typ zu ändern, kosten aber keinen Code für die Implementierung.

    – Harald

    8. Dezember 2015 um 20:25 Uhr

  • (unsigned int)3 klarer schreiben kann 3u

    – MM

    8. Dezember 2015 um 21:03 Uhr

Benutzeravatar von chqrlie
chqrlie

Ihre Frage ist wirklich faszinierend, da die nicht signierte Version durchweg Code erzeugt, der 10 bis 20 % langsamer ist. Dennoch gibt es mehrere Probleme im Code:

  • Beide Funktionen kehren zurück 0 zum 2, 3, 5 und 7was falsch ist.
  • Die Prüfung if (i != num) return 0; else return 1; ist völlig nutzlos, da der Schleifenkörper nur ausgeführt wird i < num. Ein solcher Test wäre für die kleinen Prime-Tests nützlich, aber eine spezielle Hülle ist nicht wirklich nützlich.
  • die Umwandlungen in der unsignierten Version sind überflüssig.
  • Benchmarking-Code, der eine Textausgabe an das Terminal erzeugt, unzuverlässig ist, sollten Sie die verwenden clock() Funktion, um CPU-intensive Funktionen ohne dazwischenliegende I/O zu timen.
  • Der Algorithmus für Prime-Tests ist absolut ineffizient, während die Schleife läuft num / 2 mal statt sqrt(num).

Vereinfachen wir den Code und führen einige präzise Benchmarks aus:

#include <stdio.h>
#include <time.h>

int isprime_slow(int num) {
    if (num % 2 == 0)
        return num == 2;
    for (int i = 3; i < num; i += 2) {
        if (num % i == 0)
            return 0;
    }
    return 1;
}

int unsigned_isprime_slow(unsigned int num) {
    if (num % 2 == 0)
        return num == 2;
    for (unsigned int i = 3; i < num; i += 2) {
        if (num % i == 0)
            return 0;
    }
    return 1;
}

int isprime_fast(int num) {
    if (num % 2 == 0)
        return num == 2;
    for (int i = 3; i * i <= num; i += 2) {
        if (num % i == 0)
            return 0;
    }
    return 1;
}

int unsigned_isprime_fast(unsigned int num) {
    if (num % 2 == 0)
        return num == 2;
    for (unsigned int i = 3; i * i <= num; i += 2) {
        if (num % i == 0)
            return 0;
    }
    return 1;
}

int main(void) {
    int a[] = {
        294967291, 0, 294367293, 0, 294967293, 0, 294967241, 1, 294967251, 0,
        294965291, 0, 294966291, 0, 294963293, 0, 294927293, 1, 294961293, 0,
        294917293, 0, 294167293, 0, 294267293, 0, 294367293, 0, 294467293, 0,
    };
    struct testcase { int (*fun)(); const char *name; int t; } test[] = {
        { isprime_slow, "isprime_slow", 0 },
        { unsigned_isprime_slow, "unsigned_isprime_slow", 0 },
        { isprime_fast, "isprime_fast", 0 },
        { unsigned_isprime_fast, "unsigned_isprime_fast", 0 },
    };

    for (int n = 0; n < 4; n++) {
        clock_t t = clock();
        for (int i = 0; i < 30; i += 2) {
            if (test[n].fun(a[i]) != a[i + 1]) {
                printf("%s(%d) != %d\n", test[n].name, a[i], a[i + 1]);
            }
        }
        test[n].t = clock() - t;
    }
    for (int n = 0; n < 4; n++) {
        printf("%21s: %4d.%03dms\n", test[n].name, test[n].t / 1000), test[n].t % 1000);
    }
    return 0;
}

Der Code kompiliert mit clang -O2 unter OS/X erzeugt diese Ausgabe:

         isprime_slow:  788.004ms
unsigned_isprime_slow:  965.381ms
         isprime_fast:    0.065ms
unsigned_isprime_fast:    0.089ms

Diese Timings stimmen mit dem beobachteten Verhalten des OP auf einem anderen System überein, zeigen jedoch die dramatische Verbesserung, die durch den effizienteren Iterationstest verursacht wird: 10000 mal Schneller!

Zu der Frage Warum ist die Funktion mit unsigned langsamer?schauen wir uns den generierten Code an (gcc 7,2 -O2):

isprime_slow(int):
        ...
.L5:
        movl    %edi, %eax
        cltd
        idivl   %ecx
        testl   %edx, %edx
        je      .L1
.L4:
        addl    $2, %ecx
        cmpl    %esi, %ecx
        jne     .L5
.L6:
        movl    $1, %edx
.L1:
        movl    %edx, %eax
        ret

unsigned_isprime_slow(unsigned int):
        ...
.L19:
        xorl    %edx, %edx
        movl    %edi, %eax
        divl    %ecx
        testl   %edx, %edx
        je      .L22
.L18:
        addl    $2, %ecx
        cmpl    %esi, %ecx
        jne     .L19
.L20:
        movl    $1, %eax
        ret
       ...
.L22:
        xorl    %eax, %eax
        ret

Die inneren Schleifen sind sehr ähnlich, gleiche Anzahl von Anweisungen, ähnliche Anweisungen. Hier sind jedoch einige mögliche Erklärungen:

  • cltd erweitert das Zeichen der eax registrieren Sie sich in der edx registrieren, was eine Befehlsverzögerung verursachen kann, weil eax wird durch die unmittelbar vorhergehende Anweisung modifiziert movl %edi, %eax. Dies würde die signierte Version jedoch langsamer als die unsignierte machen, nicht schneller.
  • Die anfänglichen Anweisungen der Schleifen könnten für die nicht signierte Version falsch ausgerichtet sein, aber es ist unwahrscheinlich, da eine Änderung der Reihenfolge im Quellcode keine Auswirkungen auf die Zeitabläufe hat.
  • Obwohl die Registerinhalte für die vorzeichenbehafteten und vorzeichenlosen Divisions-Opcodes identisch sind, ist es möglich, dass die idivl Anweisung dauert weniger Zyklen als die divl Anweisung. Tatsächlich arbeitet die vorzeichenbehaftete Division mit einem Bit weniger Genauigkeit als die vorzeichenlose Division, aber der Unterschied scheint für diese kleine Änderung ziemlich groß zu sein.
  • Ich vermute, dass mehr Aufwand in die Siliziumimplementierung gesteckt wurde idivl weil vorzeichenbehaftete Unterteilungen häufiger vorkommen als vorzeichenlose Unterteilungen (gemessen an jahrelangen Codierungsstatistiken bei Intel).
  • Wie von rcgldr kommentiert und sich die Anweisungstabellen für Intel-Prozesse ansieht, benötigt DIV 32 Bit für Ivy Bridge 10 Mikrooperationen, 19 bis 27 Zyklen, IDIV 9 Mikrooperationen, 19 bis 26 Zyklen. Die Benchmark-Zeiten stimmen mit diesen Timings überein. Die zusätzliche Mikrooperation kann auf die längeren Operanden in DIV (64/32 Bit) im Gegensatz zu IDIV (63/31 Bit) zurückzuführen sein.

Dieses überraschende Ergebnis sollte uns einige Lektionen beibringen:

  • Optimieren ist eine schwierige Kunst, seien Sie bescheiden und zögern Sie.
  • Korrektheit wird oft durch Optimierungen gebrochen.
  • Die Wahl eines besseren Algorithmus schlägt die Optimierung bei weitem.
  • immer Benchmark-Code, vertrauen Sie nicht Ihren Instinkten.

  • Warum nicht vorrechnen sqrt(m) abgerundet (Float-Cast in unsigned int, sagen wir, einmal und verwenden Sie das als Obergrenze in der Iteration? Das ist schneller als Testen i * i <= m

    – Henno Brandsma

    31. August 2017 um 22:16 Uhr


  • @HennoBrandsma: Es kann schneller oder nicht schneller sein, nur Benchmarking kann es sagen … Die Multiplikation ist auf modernen CPUs ziemlich schnell. Es gibt auch ein Genauigkeitsproblem für große Werte von num. Zu guter Letzt habe ich versucht, sehr einfache Funktionen zu haben, damit der Assembler-Code auch einfach bleibt.

    – chqrlie

    31. August 2017 um 22:22 Uhr


  • Diese Antwort scheint die aktuellsten Informationen zu enthalten, aber sie klärt mich immer noch nicht auf.

    – Michael Burr

    31. August 2017 um 22:36 Uhr

  • @MichaelBurr: Ich vermute, dass mehr Aufwand in die Siliziumimplementierung gesteckt wurde idivl weil vorzeichenbehaftete Divisionen häufiger sind als vorzeichenlose Divisionen (gemessen an jahrelangen Codierungsstatistiken bei Intel), aber ich habe keinen Beweis.

    – chqrlie

    31. August 2017 um 22:42 Uhr

  • Betrachtet man die Anweisungstabellen für den Intel-Prozess, so benötigt DIV 32 Bit für Ivy Bridge 10 Mikrooperationen, 19 bis 27 Zyklen, IDIV 9 Mikrooperationen, 19 bis 26 Zyklen. Unter Windows XP (mein einziges 32-Bit-Betriebssystem), Intel 3770K 3,5 GHz, Visual Studio betragen die schnellen Zeiten 0,048 ms für int, 0,065 ms für unsigned int.

    – rcgldr

    31. August 2017 um 23:12 Uhr

Benutzeravatar von shadow_map
shadow_map

Da der Überlauf von vorzeichenbehafteten Ganzzahlen undefiniert ist, kann der Compiler viele Annahmen und Optimierungen für Code treffen, der vorzeichenbehaftete Ganzzahlen enthält. Der Überlauf von vorzeichenlosen Ganzzahlen ist so definiert, dass er umbrochen wird, sodass der Compiler nicht so viel optimieren kann. Siehe auch http://blog.llvm.org/2011/05/what-every-c-programmer-should-know.html#signed_overflow und http://www.airs.com/blog/archives/120.

  • Ihre Aussagen sind korrekt, aber wenn Sie sich den von x86-Compilern generierten Code ansehen, scheint dies keine relevante Erklärung zu sein.

    – chqrlie

    31. August 2017 um 22:48 Uhr

  • Ihre Aussagen sehen richtig aus, aber es scheint die umgekehrte Frage zu beantworten. Also endlich falsch.

    – iBug

    1. September 2017 um 0:31 Uhr

Benutzeravatar von Jean-Baptiste Yunès
Jean-Baptiste Yunes

Aus Befehlsspezifikation auf AMD/Intel wir haben (für K7):

Instruction Ops Latency Throughput
DIV r32/m32 32  24      23
IDIV r32    81  41      41
IDIV m32    89  41      41 

Bei i7 sind Latenz und Durchsatz gleich IDIVL und DIVLgibt es einen kleinen Unterschied für die µops.

Dies kann den Unterschied erklären, da sich -O3-Assembly-Codes nur durch die Signiertheit (DIVL vs. IDIVL) auf meinem Computer unterscheiden.

  • Moment mal, das ist falsch herum: Das würde unsigned schneller machen

    – Harald

    8. Dezember 2015 um 20:59 Uhr

  • Aber diese Tabelle sagt, dass es für 32-Bit ohne Vorzeichen schneller sein sollte, was es nicht war (aber natürlich hat OP wahrscheinlich kein K7 mehr)

    – Harald

    8. Dezember 2015 um 21:20 Uhr

  • Dies kann die Worst-Case-Latenz für so etwas wie negative Operanden sein. Ändert sich Ihre gemessene Leistung, wenn die Operanden negativ sind?

    – Michael

    8. Dezember 2015 um 22:14 Uhr

  • das unsigned Version verwendet div was schneller sein muss

    – phuklv

    26. November 2016 um 17:19 Uhr

Alternativer Wiki-Kandidatentest, der möglicherweise einen signifikanten Zeitunterschied aufweist.

#include <stdio.h>
#include <time.h>

#define J 10
#define I 5

int main(void) {
  clock_t c1,c2,c3;
  for (int j=0; j<J; j++) {
    c1 = clock();
    for (int i=0; i<I; i++) {
      isprime(294967241);
      isprime(294367293);
    }
    c2 = clock();
    for (int i=0; i<I; i++) {
      isunsignedprime(294967241);
      isunsignedprime(294367293);
    }
    c3 = clock();
    printf("%d %d %d\n", (int)(c2-c1), (int)(c3-c2), (int)((c3-c2) - (c2-c1)));
    fflush(stdout);
  }
  return 0;
}

Beispielausgabe

2761 2746 -15
2777 2777 0
2761 2745 -16
2793 2808 15
2792 2730 -62
2746 2730 -16
2746 2730 -16
2776 2793 17
2823 2808 -15
2793 2823 30

1389030cookie-checkWarum ist “signed int” in C schneller als “unsigned int”?

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

Privacy policy