38 Stimmen

Warum ist Math.DivRem so ineffizient?

In meinem Computer dauert dieser Code 17 Sekunden (1000 Millionen Mal):

static void Main(string[] args) {
   var sw = new Stopwatch(); sw.Start();
   int r;
   for (int i = 1; i <= 100000000; i++) {
      for (int j = 1; j <= 10; j++) {
         MyDivRem (i,j, out r);
      }
   }
   Console.WriteLine(sw.ElapsedMilliseconds);
}

static int MyDivRem(int dividend, int divisor, out int remainder) {
   int quotient = dividend / divisor;
   remainder = dividend - divisor * quotient;
   return quotient;
}

während Math.DivRem 27 Sekunden dauert.

.NET Reflector gibt mir diesen Code für Math.DivRem:

public static int DivRem(int a, int b, out int result)
{
    result = a % b;
    return (a / b);
}

CIL

.method public hidebysig static int32 DivRem(int32 a, int32 b, [out] int32& result) cil managed
{
    .maxstack 8
    L_0000: ldarg.2
    L_0001: ldarg.0
    L_0002: ldarg.1
    L_0003: rem
    L_0004: stind.i4
    L_0005: ldarg.0
    L_0006: ldarg.1
    L_0007: div
    L_0008: ret
}

Theoretisch könnte es für Computer mit mehreren Kernen schneller sein, aber eigentlich sollte es überhaupt nicht notwendig sein, zwei Operationen durchzuführen, weil x86 CPUs sowohl den Quotienten als auch den Rest zurückgeben, wenn sie eine Ganzzahldivision mit DIV oder IDIV durchführen (http://www.arl.wustl.edu/~lockwood/class/cs306/books/artofasm/Chapter_6/CH06-2.html#HEADING2-451)!

0 Stimmen

Was passiert, wenn Sie .NET auf Nicht-x86 ausführen?

0 Stimmen

X64 ist eine Erweiterung von x86 und wenn die CPU nicht kompatibel mit x86 ist, handelt es sich lediglich um die Verwendung eines anderen Codes für das .net-Framework für diese CPU

0 Stimmen

Richtig, aber .NET ist auch als Mono implementiert und sollte daher auf anderen Architekturen wie ppc usw. laufen.

20voto

Joshua Punkte 37898

Grrr. Der einzige Grund für das Bestehen dieser Funktion besteht darin, die CPU-Anweisung dafür zu nutzen, und sie haben es noch nicht einmal gemacht!

1 Stimmen

Müsstest du nicht den IL anschauen, um das zu beweisen?

2 Stimmen

Nein. Wenn sie es so gemacht hätten, würde Ihnen .NET Reflector sagen, dass diese Funktion in den nativen Stubs ist.

0 Stimmen

Zur Info: Der Jitter macht das in .NET 4 tatsächlich nicht (In der Disassemblierung sind zwei idivs, zwei cdqs und eine ganze Menge an movs enthalten).

15voto

Bob Punkte 13881

Während .NET Framework 4.6.2 immer noch das suboptimale Modulo und die Division verwendet, ersetzt .NET Core (CoreCLR) derzeit die Division durch eine Subtraktion:

    public static int DivRem(int a, int b, out int result)
    {
        // TODO https://github.com/dotnet/runtime/issues/5213:
        // Wiederherstellen der Verwendung von % und /, wenn der JIT in der Lage ist, eines der idivs zu eliminieren.
        // In der Zwischenzeit ist * und - messbar schneller als ein zusätzliches /.

        int div = a / b;
        result = a - (div * b);
        return div;
    }

Und es gibt ein offenes Problem, entweder speziell DivRem zu verbessern (via Intrinsik), oder den allgemeinen Fall in RyuJIT zu erkennen und zu optimieren.

0 Stimmen

Warum hat er Klammern um div * b gesetzt?

0 Stimmen

@jnm2: Nun, ich dachte, man sollte sich auf die Operatorpriorität verlassen und Klammern nur verwenden, wenn die beabsichtigte Priorität von der in der Sprache verwendeten abweicht...

0 Stimmen

@Ant_222 Ich stimme dem nicht zu. Die übliche Richtlinie ist, zuerst für menschliche Leser zu optimieren. Alles, was den Code klarer macht, ist vorzuziehen.

14voto

Die in Sente Punkte 9197

Wow, das sieht wirklich dumm aus, oder?

Das Problem ist, dass laut dem Microsoft Press Buch ".NET IL Assembler" von Lidin die IL-rem- und div-arithmetischen Anweisungen genau das sind: Rest berechnen und Divisor berechnen.

Alle arithmetischen Operationen außer der Negationsoperation nehmen zwei Operanden vom Stapel und legen das Ergebnis auf den Stapel.

Angeblich ist es aufgrund der Struktur der IL-Assembler-Sprache nicht möglich, eine IL-Anweisung zu haben, die zwei Ausgaben erzeugt und sie auf den Eval-Stapel drückt. Angesichts dieser Einschränkung kann man in der IL-Assembler keine Divisionsanweisung haben, die beide Berechnungen genauso durchführt wie die x86-DIV- oder IDIV-Anweisungen.

IL wurde für Sicherheit, Überprüfbarkeit und Stabilität entworfen, NICHT für Leistung. Jeder, der eine rechenintensive Anwendung hat und hauptsächlich an Leistung interessiert ist, wird nativen Code verwenden und nicht .NET.

Ich habe kürzlich an der Supercomputing '08 teilgenommen, und in einer der technischen Sitzungen sagte ein Evangelist für Microsoft Compute Server die Faustregel, dass .NET in der Regel halb so schnell wie nativer Code ist - was genau hier der Fall ist!

10 Stimmen

Während dies zutrifft, gibt es keinen Grund, warum Math.DivRem nicht im Laufzeitumgebung implementiert und mit [MethodImpl(MethodImplOptions.InternalCall), SecuritySafeCritical] markiert werden kann, so wie es bei vielen anderen Methoden auf System.Math der Fall ist.

2 Stimmen

Hast du eine Quelle für die Behauptung, dass IL grundsätzlich nicht in der Lage ist, zwei Werte auf dem Stack zu produzieren? Ich sage nicht, dass es falsch ist; es ist leicht vorstellbar, dass der JITter diese Annahme intensiv nutzt, aber eine ordentliche Quelle wäre hilfreich.

1 Stimmen

Das ist wahr, aber DivRem könnte eine einzelne Struktur mit den Feldern Div und Rem zurückgeben. Es scheint sowieso schneller zu sein, und eine Methode ohne out-Parameter sollte besser sein. Nun, ich denke, das ist nicht die Frage in .Net.

3voto

rmeador Punkte 25087

Wenn ich raten müsste, würde ich sagen, dass die Person, die Math.DivRem implementiert hat, keine Ahnung hatte, dass x86-Prozessoren dies in einer einzigen Anweisung durchführen können, und es daher als zwei Operationen geschrieben hat. Das ist nicht unbedingt schlecht, wenn der Optimierer richtig funktioniert, obwohl es ein weiterer Hinweis darauf ist, dass es heutzutage den meisten Programmierern an grundlegendem Wissen mangelt. Ich würde erwarten, dass der Optimierer den Modulus zusammenführt und dann die Divisionsoperationen in eine Anweisung umwandelt, und die Personen, die Optimierer schreiben, sollten solche grundlegenden Dinge kennen...

1 Stimmen

Genauso wie der Compiler oder der JIT Math.Pow(x,3) mit x_x_x ersetzen sollte, aber das scheint zu viel verlangt zu sein...

2voto

Sander Punkte 24565

Die Antwort lautet wahrscheinlich, dass niemand dies als Priorität betrachtet hat - es ist gut genug. Die Tatsache, dass dies mit keiner neuen Version des .NET Frameworks behoben wurde, ist ein Indikator dafür, wie selten dies verwendet wird - höchstwahrscheinlich hat sich noch nie jemand beschwert.

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X