In C# gibt es einen signifikanten Leistungsunterschied für die Verwendung von UInt32 vs. Int32

In C# gibt es einen signifikanten Leistungsunterschied für die Verwendung von UInt32 vs. Int32

Die kurze Antwort lautet "Nein. Jegliche Auswirkungen auf die Leistung sind vernachlässigbar".

Die richtige Antwort ist "Es kommt darauf an."

Eine bessere Frage ist:"Soll ich uint verwenden, wenn ich sicher bin, dass ich kein Zeichen brauche?"

Der Grund, warum Sie in Bezug auf die Leistung kein endgültiges „Ja“ oder „Nein“ geben können, liegt darin, dass die Zielplattform letztendlich die Leistung bestimmt. Das heißt, die Leistung wird von dem Prozessor bestimmt, der den Code ausführen wird, und von den verfügbaren Anweisungen. Ihr .NET-Code wird in Intermediate Language (IL oder Bytecode) kompiliert. Diese Anweisungen werden dann vom Just-In-Time (JIT)-Compiler als Teil der Common Language Runtime (CLR) auf der Zielplattform kompiliert. Sie können nicht kontrollieren oder vorhersagen, welcher Code für jeden Benutzer generiert wird.

Wenn man also weiß, dass die Hardware der letzte Entscheidungsfaktor für die Leistung ist, stellt sich die Frage:„Wie unterschiedlich ist der Code, den .NET für eine vorzeichenbehaftete gegenüber einer vorzeichenlosen Ganzzahl generiert?“ und "Wirkt sich der Unterschied auf meine Anwendung und meine Zielplattformen aus?"

Diese Fragen lassen sich am besten mit einem Test beantworten.

class Program
{
  static void Main(string[] args)
  {
    const int iterations = 100;
    Console.WriteLine($"Signed:      {Iterate(TestSigned, iterations)}");
    Console.WriteLine($"Unsigned:    {Iterate(TestUnsigned, iterations)}");
    Console.Read();
  }

  private static void TestUnsigned()
  {
    uint accumulator = 0;
    var max = (uint)Int32.MaxValue;
    for (uint i = 0; i < max; i++) ++accumulator;
  }

  static void TestSigned()
  {
    int accumulator = 0;
    var max = Int32.MaxValue;
    for (int i = 0; i < max; i++) ++accumulator;
  }

  static TimeSpan Iterate(Action action, int count)
  {
    var elapsed = TimeSpan.Zero;
    for (int i = 0; i < count; i++)
      elapsed += Time(action);
    return new TimeSpan(elapsed.Ticks / count);
  }

  static TimeSpan Time(Action action)
  {
    var sw = new Stopwatch();
    sw.Start();
    action();
    sw.Stop();
    return sw.Elapsed;
  }
}

Die zwei Testmethoden, TestSigned und TestUnsigned , führen jeweils ~2 Millionen Iterationen eines einfachen Inkrements für eine vorzeichenbehaftete bzw. vorzeichenlose Ganzzahl aus. Der Testcode führt 100 Iterationen jedes Tests durch und mittelt die Ergebnisse. Dies sollte mögliche Inkonsistenzen ausmerzen. Die Ergebnisse auf meinem für x64 kompilierten i7-5960X waren:

Signed:      00:00:00.5066966

Unsigned:    00:00:00.5052279

Diese Ergebnisse sind nahezu identisch, aber um eine endgültige Antwort zu erhalten, müssen wir uns wirklich den für das Programm generierten Bytecode ansehen. Wir können ILDASM als Teil des .NET SDK verwenden, um den Code in der vom Compiler generierten Assembly zu untersuchen.

Hier können wir sehen, dass der C#-Compiler vorzeichenbehaftete Ganzzahlen bevorzugt und tatsächlich die meisten Operationen nativ als vorzeichenbehaftete Ganzzahlen ausführt und den Wert im Speicher immer nur als vorzeichenlos behandelt, wenn er für die Verzweigung vergleicht (auch bekannt als Sprung oder wenn). Trotz der Tatsache, dass wir in TestUnsigned sowohl für den Iterator als auch für den Akkumulator eine Ganzzahl ohne Vorzeichen verwenden , ist der Code nahezu identisch mit TestSigned Methode bis auf eine einzelne Anweisung:IL_0016 . Ein kurzer Blick auf die ECMA-Spezifikation beschreibt den Unterschied:

Da es sich um eine so häufige Anweisung handelt, kann man davon ausgehen, dass die meisten modernen Hochleistungsprozessoren Hardwareanweisungen für beide Operationen haben und sehr wahrscheinlich in der gleichen Anzahl von Zyklen ausgeführt werden, aber dies ist nicht garantiert . Ein Low-Power-Prozessor hat möglicherweise weniger Anweisungen und keinen Zweig für unsigned int. In diesem Fall muss der JIT-Compiler möglicherweise mehrere Hardwareanweisungen ausgeben (z. B. zuerst eine Konvertierung, dann eine Verzweigung), um blt.un.s auszuführen IL-Anweisung. Selbst wenn dies der Fall ist, wären diese zusätzlichen Anweisungen grundlegend und würden die Leistung wahrscheinlich nicht wesentlich beeinträchtigen.

In Bezug auf die Leistung lautet die lange Antwort also:„Es ist unwahrscheinlich, dass es überhaupt einen Leistungsunterschied zwischen der Verwendung einer vorzeichenbehafteten oder einer vorzeichenlosen Ganzzahl gibt. Wenn es einen Unterschied gibt, ist er wahrscheinlich vernachlässigbar.“

Wenn also die Leistung identisch ist, lautet die nächste logische Frage:„Soll ich einen vorzeichenlosen Wert verwenden, wenn ich sicher bin, dass ich ihn nicht brauche ein Zeichen?"

Hier sind zwei Dinge zu beachten:Erstens sind Ganzzahlen ohne Vorzeichen NICHT CLS-kompatibel, was bedeutet, dass Sie möglicherweise auf Probleme stoßen, wenn Sie eine Ganzzahl ohne Vorzeichen als Teil einer API verfügbar machen, die von einem anderen Programm verwendet wird (z Verteilung einer wiederverwendbaren Bibliothek). Zweitens verwenden die meisten Operationen in .NET, einschließlich der von der BCL bereitgestellten Methodensignaturen (aus dem oben genannten Grund), eine vorzeichenbehaftete Ganzzahl. Wenn Sie also vorhaben, Ihre vorzeichenlose Ganzzahl tatsächlich zu verwenden, werden Sie wahrscheinlich feststellen, dass Sie sie ziemlich oft umwandeln. Dies wird einen sehr kleinen Leistungseinbruch haben und Ihren Code ein wenig chaotischer machen. Am Ende lohnt es sich wahrscheinlich nicht.

TLDR; In meinen C++-Tagen würde ich sagen:"Verwenden Sie, was am besten geeignet ist, und lassen Sie den Compiler den Rest regeln." C# ist nicht ganz so ausgereift, also würde ich das für .NET sagen:Es gibt wirklich keinen Leistungsunterschied zwischen einer vorzeichenbehafteten und einer vorzeichenlosen Ganzzahl auf x86/x64, aber die meisten Operationen erfordern eine vorzeichenbehaftete Ganzzahl, es sei denn, Sie MÜSSEN es wirklich Beschränken Sie die Werte NUR auf positiv oder Sie BENÖTIGEN wirklich den zusätzlichen Bereich, den das Vorzeichenbit frisst, bleiben Sie bei einer vorzeichenbehafteten Ganzzahl. Ihr Code wird am Ende sauberer sein.


Ich glaube nicht, dass es irgendwelche Überlegungen zur Leistung gibt, außer einem möglichen Unterschied zwischen vorzeichenbehafteter und vorzeichenloser Arithmetik auf Prozessorebene, aber an diesem Punkt denke ich, dass die Unterschiede strittig sind.

Der größere Unterschied liegt in der CLS-Konformität, da die unsignierten Typen nicht CLS-konform sind, da sie nicht von allen Sprachen unterstützt werden.


Ich habe in .NET keine Nachforschungen zu diesem Thema angestellt, aber in den alten Tagen von Win32/C++ musste die CPU eine Operation zum Erweitern ausführen, wenn Sie ein "signed int" in ein "signed long" umwandeln wollten das Schild. Um ein "unsigned int" in ein "unsigned long" umzuwandeln, hatte es nur Stuff Zero in den oberen Bytes. Die Einsparungen lagen in der Größenordnung von ein paar Taktzyklen (d. h. Sie müssten es Milliarden Male tun, um einen sogar wahrnehmbaren Unterschied zu haben)