it-swarm.com.de

Wann muss der Speicher für die Leistung für eine Methode optimiert werden?

Ich habe kürzlich bei Amazon interviewt. Während einer Codierungssitzung fragte der Interviewer, warum ich eine Variable in einer Methode deklariert habe. Ich erklärte meinen Prozess und er forderte mich auf, dasselbe Problem mit weniger Variablen zu lösen. Zum Beispiel (dies war nicht aus dem Interview) begann ich mit Methode A dann verbessert es zu Methode B , = durch Entfernen von int s. Er war erfreut und sagte, dies würde die Speichernutzung durch diese Methode reduzieren.

Ich verstehe die Logik dahinter, aber meine Frage ist:

Wann ist es angebracht, Methode A gegen Methode B anzuwenden und umgekehrt?

Sie können sehen, dass Methode A Eine höhere Speichernutzung haben wird, da int s wird deklariert, muss jedoch nur eine Berechnung durchführen, d. h. a + b. Andererseits hat Methode B eine geringere Speichernutzung, muss jedoch zwei Berechnungen durchführen, d. H. a + b zweimal. Wann verwende ich eine Technik über der anderen? Oder wird eine der Techniken immer der anderen vorgezogen? Was ist bei der Bewertung der beiden Methoden zu beachten?

Methode A:

private bool IsSumInRange(int a, int b)
{
    int s = a + b;

    if (s > 1000 || s < -1000) return false;
    else return true;
}

Methode B:

private bool IsSumInRange(int a, int b)
{
    if (a + b > 1000 || a + b < -1000) return false;
    else return true;
}
109
Corey P

Anstatt darüber zu spekulieren, was passieren kann oder nicht, schauen wir doch mal, sollen wir? Ich muss C++ verwenden, da ich keinen C # -Compiler zur Hand habe (obwohl siehe C # -Beispiel von VisualMelon ), aber ich bin sicher, dass die gleichen Prinzipien gelten ungeachtet.

Wir werden die beiden Alternativen, auf die Sie gestoßen sind, in das Interview aufnehmen. Wir werden auch eine Version hinzufügen, die abs verwendet, wie in einigen Antworten vorgeschlagen.

#include <cstdlib>

bool IsSumInRangeWithVar(int a, int b)
{
    int s = a + b;

    if (s > 1000 || s < -1000) return false;
    else return true;
}

bool IsSumInRangeWithoutVar(int a, int b)
{
    if (a + b > 1000 || a + b < -1000) return false;
    else return true;
}

bool IsSumInRangeSuperOptimized(int a, int b) {
    return (abs(a + b) < 1000);
}

Kompilieren Sie es jetzt ohne jegliche Optimierung: g++ -c -o test.o test.cpp

Jetzt können wir genau sehen, was dies erzeugt: objdump -d test.o

0000000000000000 <_Z19IsSumInRangeWithVarii>:
   0:   55                      Push   %rbp              # begin a call frame
   1:   48 89 e5                mov    %rsp,%rbp
   4:   89 7d ec                mov    %edi,-0x14(%rbp)  # save first argument (a) on stack
   7:   89 75 e8                mov    %esi,-0x18(%rbp)  # save b on stack
   a:   8b 55 ec                mov    -0x14(%rbp),%edx  # load a and b into edx
   d:   8b 45 e8                mov    -0x18(%rbp),%eax  # load b into eax
  10:   01 d0                   add    %edx,%eax         # add a and b
  12:   89 45 fc                mov    %eax,-0x4(%rbp)   # save result as s on stack
  15:   81 7d fc e8 03 00 00    cmpl   $0x3e8,-0x4(%rbp) # compare s to 1000
  1c:   7f 09                   jg     27                # jump to 27 if it's greater
  1e:   81 7d fc 18 fc ff ff    cmpl   $0xfffffc18,-0x4(%rbp) # compare s to -1000
  25:   7d 07                   jge    2e                # jump to 2e if it's greater or equal
  27:   b8 00 00 00 00          mov    $0x0,%eax         # put 0 (false) in eax, which will be the return value
  2c:   eb 05                   jmp    33 <_Z19IsSumInRangeWithVarii+0x33>
  2e:   b8 01 00 00 00          mov    $0x1,%eax         # put 1 (true) in eax
  33:   5d                      pop    %rbp
  34:   c3                      retq

0000000000000035 <_Z22IsSumInRangeWithoutVarii>:
  35:   55                      Push   %rbp
  36:   48 89 e5                mov    %rsp,%rbp
  39:   89 7d fc                mov    %edi,-0x4(%rbp)
  3c:   89 75 f8                mov    %esi,-0x8(%rbp)
  3f:   8b 55 fc                mov    -0x4(%rbp),%edx
  42:   8b 45 f8                mov    -0x8(%rbp),%eax  # same as before
  45:   01 d0                   add    %edx,%eax
  # note: unlike other implementation, result is not saved
  47:   3d e8 03 00 00          cmp    $0x3e8,%eax      # compare to 1000
  4c:   7f 0f                   jg     5d <_Z22IsSumInRangeWithoutVarii+0x28>
  4e:   8b 55 fc                mov    -0x4(%rbp),%edx  # since s wasn't saved, load a and b from the stack again
  51:   8b 45 f8                mov    -0x8(%rbp),%eax
  54:   01 d0                   add    %edx,%eax
  56:   3d 18 fc ff ff          cmp    $0xfffffc18,%eax # compare to -1000
  5b:   7d 07                   jge    64 <_Z22IsSumInRangeWithoutVarii+0x2f>
  5d:   b8 00 00 00 00          mov    $0x0,%eax
  62:   eb 05                   jmp    69 <_Z22IsSumInRangeWithoutVarii+0x34>
  64:   b8 01 00 00 00          mov    $0x1,%eax
  69:   5d                      pop    %rbp
  6a:   c3                      retq

000000000000006b <_Z26IsSumInRangeSuperOptimizedii>:
  6b:   55                      Push   %rbp
  6c:   48 89 e5                mov    %rsp,%rbp
  6f:   89 7d fc                mov    %edi,-0x4(%rbp)
  72:   89 75 f8                mov    %esi,-0x8(%rbp)
  75:   8b 55 fc                mov    -0x4(%rbp),%edx
  78:   8b 45 f8                mov    -0x8(%rbp),%eax
  7b:   01 d0                   add    %edx,%eax
  7d:   3d 18 fc ff ff          cmp    $0xfffffc18,%eax
  82:   7c 16                   jl     9a <_Z26IsSumInRangeSuperOptimizedii+0x2f>
  84:   8b 55 fc                mov    -0x4(%rbp),%edx
  87:   8b 45 f8                mov    -0x8(%rbp),%eax
  8a:   01 d0                   add    %edx,%eax
  8c:   3d e8 03 00 00          cmp    $0x3e8,%eax
  91:   7f 07                   jg     9a <_Z26IsSumInRangeSuperOptimizedii+0x2f>
  93:   b8 01 00 00 00          mov    $0x1,%eax
  98:   eb 05                   jmp    9f <_Z26IsSumInRangeSuperOptimizedii+0x34>
  9a:   b8 00 00 00 00          mov    $0x0,%eax
  9f:   5d                      pop    %rbp
  a0:   c3                      retq

Wir können anhand der Stapeladressen (zum Beispiel -0x4 In mov %edi,-0x4(%rbp) gegenüber -0x14 In mov %edi,-0x14(%rbp)) erkennen, dass IsSumInRangeWithVar() verwendet 16 zusätzliche Bytes auf dem Stapel.

Da IsSumInRangeWithoutVar() keinen Speicherplatz auf dem Stapel zum Speichern des Zwischenwerts s reserviert, muss dieser neu berechnet werden, was dazu führt, dass diese Implementierung 2 Anweisungen länger ist.

Witzig, IsSumInRangeSuperOptimized() sieht IsSumInRangeWithoutVar() sehr ähnlich, außer dass es mit -1000 zuerst und 1000 Sekunden verglichen wird.

Lassen Sie uns nun nur mit den grundlegendsten Optimierungen kompilieren: g++ -O1 -c -o test.o test.cpp. Das Ergebnis:

0000000000000000 <_Z19IsSumInRangeWithVarii>:
   0:   8d 84 37 e8 03 00 00    lea    0x3e8(%rdi,%rsi,1),%eax
   7:   3d d0 07 00 00          cmp    $0x7d0,%eax
   c:   0f 96 c0                setbe  %al
   f:   c3                      retq

0000000000000010 <_Z22IsSumInRangeWithoutVarii>:
  10:   8d 84 37 e8 03 00 00    lea    0x3e8(%rdi,%rsi,1),%eax
  17:   3d d0 07 00 00          cmp    $0x7d0,%eax
  1c:   0f 96 c0                setbe  %al
  1f:   c3                      retq

0000000000000020 <_Z26IsSumInRangeSuperOptimizedii>:
  20:   8d 84 37 e8 03 00 00    lea    0x3e8(%rdi,%rsi,1),%eax
  27:   3d d0 07 00 00          cmp    $0x7d0,%eax
  2c:   0f 96 c0                setbe  %al
  2f:   c3                      retq

Würden Sie sich das ansehen: Jede Variante ist identisch. Der Compiler kann etwas ziemlich Kluges tun: abs(a + b) <= 1000 entspricht a + b + 1000 <= 2000, Wenn man bedenkt, dass setbe einen vorzeichenlosen Vergleich durchführt, sodass eine negative Zahl zu einer sehr großen positiven Zahl wird. Der Befehl lea kann tatsächlich alle diese Ergänzungen in einem Befehl ausführen und alle bedingten Verzweigungen beseitigen.

Um Ihre Frage zu beantworten: fast immer Das zu optimierende Element ist nicht Speicher oder Geschwindigkeit, sondern Lesbarkeit. Das Lesen von Code ist viel schwieriger als das Schreiben, und das Lesen von Code, der zur "Optimierung" entstellt wurde, ist viel schwieriger als das Lesen von Code, der klar geschrieben wurde. Meistens haben diese "Optimierungen" vernachlässigbare oder wie in diesem Fall genau Null tatsächliche Auswirkungen auf die Leistung.


Folgefrage: Was ändert sich, wenn dieser Code in einer interpretierten Sprache anstatt kompiliert ist? Ist dann die Optimierung wichtig oder hat sie das gleiche Ergebnis?

Lass uns messen! Ich habe die Beispiele in Python transkribiert:

def IsSumInRangeWithVar(a, b):
    s = a + b
    if s > 1000 or s < -1000:
        return False
    else:
        return True

def IsSumInRangeWithoutVar(a, b):
    if a + b > 1000 or a + b < -1000:
        return False
    else:
        return True

def IsSumInRangeSuperOptimized(a, b):
    return abs(a + b) <= 1000

from dis import dis
print('IsSumInRangeWithVar')
dis(IsSumInRangeWithVar)

print('\nIsSumInRangeWithoutVar')
dis(IsSumInRangeWithoutVar)

print('\nIsSumInRangeSuperOptimized')
dis(IsSumInRangeSuperOptimized)

print('\nBenchmarking')
import timeit
print('IsSumInRangeWithVar: %fs' % (min(timeit.repeat(lambda: IsSumInRangeWithVar(42, 42), repeat=50, number=100000)),))
print('IsSumInRangeWithoutVar: %fs' % (min(timeit.repeat(lambda: IsSumInRangeWithoutVar(42, 42), repeat=50, number=100000)),))
print('IsSumInRangeSuperOptimized: %fs' % (min(timeit.repeat(lambda: IsSumInRangeSuperOptimized(42, 42), repeat=50, number=100000)),))

Führen Sie mit Python 3.5.2) aus, dies erzeugt die Ausgabe:

IsSumInRangeWithVar
  2           0 LOAD_FAST                0 (a)
              3 LOAD_FAST                1 (b)
              6 BINARY_ADD
              7 STORE_FAST               2 (s)

  3          10 LOAD_FAST                2 (s)
             13 LOAD_CONST               1 (1000)
             16 COMPARE_OP               4 (>)
             19 POP_JUMP_IF_TRUE        34
             22 LOAD_FAST                2 (s)
             25 LOAD_CONST               4 (-1000)
             28 COMPARE_OP               0 (<)
             31 POP_JUMP_IF_FALSE       38

  4     >>   34 LOAD_CONST               2 (False)
             37 RETURN_VALUE

  6     >>   38 LOAD_CONST               3 (True)
             41 RETURN_VALUE
             42 LOAD_CONST               0 (None)
             45 RETURN_VALUE

IsSumInRangeWithoutVar
  9           0 LOAD_FAST                0 (a)
              3 LOAD_FAST                1 (b)
              6 BINARY_ADD
              7 LOAD_CONST               1 (1000)
             10 COMPARE_OP               4 (>)
             13 POP_JUMP_IF_TRUE        32
             16 LOAD_FAST                0 (a)
             19 LOAD_FAST                1 (b)
             22 BINARY_ADD
             23 LOAD_CONST               4 (-1000)
             26 COMPARE_OP               0 (<)
             29 POP_JUMP_IF_FALSE       36

 10     >>   32 LOAD_CONST               2 (False)
             35 RETURN_VALUE

 12     >>   36 LOAD_CONST               3 (True)
             39 RETURN_VALUE
             40 LOAD_CONST               0 (None)
             43 RETURN_VALUE

IsSumInRangeSuperOptimized
 15           0 LOAD_GLOBAL              0 (abs)
              3 LOAD_FAST                0 (a)
              6 LOAD_FAST                1 (b)
              9 BINARY_ADD
             10 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             13 LOAD_CONST               1 (1000)
             16 COMPARE_OP               1 (<=)
             19 RETURN_VALUE

Benchmarking
IsSumInRangeWithVar: 0.019361s
IsSumInRangeWithoutVar: 0.020917s
IsSumInRangeSuperOptimized: 0.020171s

Die Demontage in Python ist nicht besonders interessant, da der Bytecode "Compiler" nicht viel zur Optimierung beiträgt.

Die Leistung der drei Funktionen ist nahezu identisch. Wir könnten versucht sein, mit IsSumInRangeWithVar() zu arbeiten, da es nur einen geringen Geschwindigkeitsgewinn gibt. Obwohl ich hinzufügen werde, als ich verschiedene Parameter zu timeit ausprobiert habe, kam IsSumInRangeSuperOptimized() manchmal am schnellsten heraus, so dass ich vermute, dass es externe Faktoren sind, die für den Unterschied verantwortlich sind, und nicht irgendein intrinsischer Vorteil von jede Implementierung.

Wenn dies wirklich leistungskritischer Code ist, ist eine interpretierte Sprache einfach eine sehr schlechte Wahl. Wenn ich das gleiche Programm mit pypy starte, bekomme ich:

IsSumInRangeWithVar: 0.000180s
IsSumInRangeWithoutVar: 0.001175s
IsSumInRangeSuperOptimized: 0.001306s

Allein die Verwendung von pypy, bei dem mithilfe der JIT-Kompilierung ein Großteil des Interpreter-Overheads vermieden wird, hat zu einer Leistungsverbesserung von 1 oder 2 Größenordnungen geführt. Ich war ziemlich schockiert zu sehen, dass IsSumInRangeWithVar() eine Größenordnung schneller ist als die anderen. Also habe ich die Reihenfolge der Benchmarks geändert und bin wieder gelaufen:

IsSumInRangeSuperOptimized: 0.000191s
IsSumInRangeWithoutVar: 0.001174s
IsSumInRangeWithVar: 0.001265s

Es scheint also nicht irgendetwas an der Implementierung zu sein, das es schnell macht, sondern an der Reihenfolge, in der ich das Benchmarking durchführe!

Ich würde mich gerne eingehender damit befassen, weil ich ehrlich gesagt nicht weiß, warum dies passiert. Ich glaube jedoch, dass der Punkt klargestellt wurde: Mikrooptimierungen wie die Angabe eines Zwischenwerts als Variable sind selten relevant. Bei einer interpretierten Sprache oder einem hochoptimierten Compiler besteht das erste Ziel immer noch darin, klaren Code zu schreiben.

Wenn weitere Optimierungen erforderlich sein könnten, Benchmark. Denken Sie daran, dass die besten Optimierungen nicht auf den kleinen Details beruhen, sondern auf dem größeren algorithmischen Bild: pypy wird für die wiederholte Auswertung derselben Funktion um eine Größenordnung schneller sein als cpython, da es schnellere Algorithmen (JIT-Compiler vs. Interpretation) zur Auswertung verwendet Programm. Und es gibt auch den codierten Algorithmus, der berücksichtigt werden muss: Eine Suche in einem B-Baum ist schneller als eine verknüpfte Liste.

Nachdem Sie sichergestellt haben, dass Sie die richtigen Tools und Algorithmen für den Job verwenden, sollten Sie sich darauf einstellen, deep in die Details des Systems einzutauchen. Die Ergebnisse können selbst für erfahrene Entwickler sehr überraschend sein. Aus diesem Grund müssen Sie einen Benchmark haben, um die Änderungen zu quantifizieren.

147
Phil Frost

Um die angegebene Frage zu beantworten:

Wann muss der Speicher für eine Methode im Vergleich zur Leistungsgeschwindigkeit optimiert werden?

Es gibt zwei Dinge, die Sie festlegen müssen:

  • Was schränkt Ihre Bewerbung ein?
  • Wo kann ich den größten Teil dieser Ressource zurückfordern?

Um die erste Frage zu beantworten, müssen Sie die Leistungsanforderungen für Ihre Anwendung kennen. Wenn es keine Leistungsanforderungen gibt, gibt es keinen Grund, auf die eine oder andere Weise zu optimieren. Die Leistungsanforderungen helfen Ihnen, an den Ort "gut genug" zu gelangen.

Die Methode, die Sie selbst bereitgestellt haben, würde für sich genommen keine Leistungsprobleme verursachen, aber möglicherweise müssen Sie innerhalb einer Schleife und bei der Verarbeitung einer großen Datenmenge etwas anders darüber nachdenken, wie Sie das Problem angehen.

Erkennen, was die Anwendung einschränkt

Sehen Sie sich das Verhalten Ihrer Anwendung mit einem Leistungsmonitor an. Behalten Sie die CPU-, Festplatten-, Netzwerk- und Speichernutzung im Auge, während sie ausgeführt wird. Ein oder mehrere Elemente werden maximal genutzt, während alles andere nur mäßig verwendet wird - es sei denn, Sie treffen die perfekte Balance, aber das passiert fast nie.

Wenn Sie genauer hinschauen müssen, verwenden Sie normalerweise einen Profiler . Es gibt Speicherprofiler und Prozessprofiler , die verschiedene Dinge messen. Das Erstellen von Profilen hat erhebliche Auswirkungen auf die Leistung, aber Sie instrumentieren Ihren Code, um herauszufinden, was falsch ist.

Angenommen, Sie sehen, dass Ihre CPU- und Festplattenauslastung ihren Höhepunkt erreicht hat. Sie würden zuerst nach "Hot Spots" oder Code suchen, der entweder häufiger als der Rest aufgerufen wird oder einen wesentlich längeren Prozentsatz der Verarbeitung in Anspruch nimmt.

Wenn Sie keine Hot Spots finden, schauen Sie sich das Gedächtnis an. Vielleicht erstellen Sie mehr Objekte als nötig und Ihre Garbage Collection macht Überstunden.

Leistung zurückfordern

Denken Sie kritisch. Die folgende Liste der Änderungen ist in der Reihenfolge der Kapitalrendite aufgeführt:

  • Architektur: Suchen Sie nach Kommunikationsdrosselstellen
  • Algorithmus: Die Art und Weise, wie Sie Daten verarbeiten, muss sich möglicherweise ändern
  • Hot Spots: Wenn Sie minimieren, wie oft Sie den Hot Spot anrufen, erhalten Sie einen großen Bonus
  • Mikrooptimierungen: Es ist nicht üblich, aber manchmal müssen Sie wirklich an kleinere Änderungen denken (wie das von Ihnen bereitgestellte Beispiel), insbesondere wenn es sich um einen Hot Spot in Ihrem Code handelt.

In solchen Situationen müssen Sie die wissenschaftliche Methode anwenden. Überlegen Sie sich eine Hypothese, nehmen Sie die Änderungen vor und testen Sie sie. Wenn Sie Ihre Leistungsziele erreichen, sind Sie fertig. Wenn nicht, fahren Sie mit dem nächsten Punkt in der Liste fort.


Beantwortung der Frage in Fettdruck:

Wann ist es angebracht, Methode A gegen Methode B anzuwenden und umgekehrt?

Ehrlich gesagt ist dies der letzte Schritt, um mit Leistungs- oder Speicherproblemen umzugehen. Die Auswirkungen von Methode A gegenüber Methode B sind je nach Sprache nd Plattform (in einigen Fällen) sehr unterschiedlich.

Nahezu jede kompilierte Sprache mit einem halbwegs anständigen Optimierer generiert mit jeder dieser Strukturen ähnlichen Code. Diese Annahmen gelten jedoch nicht unbedingt für proprietäre Sprachen und Spielzeugsprachen ohne Optimierer.

Welche Auswirkungen sich besser auswirken, hängt davon ab, ob sum eine Stapelvariable oder eine Heap-Variable ist. Dies ist eine Sprachimplementierungsoption. In C, C++ und Java zum Beispiel sind Zahlenprimitive wie ein int standardmäßig Stapelvariablen. Ihr Code hat durch die Zuweisung zu einer Stapelvariablen keine größere Auswirkung auf den Speicher als Sie habe mit voll inline Code.

Andere Optimierungen, die Sie möglicherweise in C-Bibliotheken finden (insbesondere in älteren), bei denen Sie sich entscheiden müssen, ob Sie ein zweidimensionales Array zuerst nach unten oder zuerst kopieren möchten, sind plattformabhängige Optimierungen. Es erfordert einige Kenntnisse darüber, wie der Chipsatz, auf den Sie abzielen, den Speicherzugriff am besten optimiert. Es gibt subtile Unterschiede zwischen Architekturen.

Fazit ist, dass Optimierung eine Kombination aus Kunst und Wissenschaft ist. Es erfordert kritisches Denken sowie ein gewisses Maß an Flexibilität bei der Herangehensweise an das Problem. Suchen Sie nach großen Dingen, bevor Sie kleine Dinge beschuldigen.

66
Berin Loritsch

"das würde den Speicher reduzieren" - em, nein. Selbst wenn dies wahr wäre (was für jeden anständigen Compiler nicht der Fall ist), wäre der Unterschied für jede reale Situation höchstwahrscheinlich vernachlässigbar.

Ich würde jedoch empfehlen, Methode A * zu verwenden (Methode A mit einer geringfügigen Änderung):

private bool IsSumInRange(int a, int b)
{
    int sum = a + b;

    if (sum > 1000 || sum < -1000) return false;
    else return true;
    // (yes, the former statement could be cleaned up to
    // return abs(sum)<=1000;
    // but let's ignore this for a moment)
}

aber aus zwei völlig unterschiedlichen Gründen:

  • wenn Sie der Variablen s einen erklärenden Namen geben, wird der Code klarer

  • es wird vermieden, dass dieselbe Summierungslogik zweimal im Code vorhanden ist, sodass der Code trockener wird, was bedeutet, dass weniger Fehler für Änderungen anfällig sind.

45
Doc Brown

Sie können es besser machen als beide mit

return (abs(a + b) > 1000);

Die meisten Prozessoren (und damit Compiler) können abs () in einem einzigen Vorgang ausführen. Sie haben nicht nur weniger Summen, sondern auch weniger Vergleiche, die im Allgemeinen rechenintensiver sind. Außerdem wird die Verzweigung entfernt, was bei den meisten Prozessoren viel schlimmer ist, da kein Pipelining mehr möglich ist.

Der Interviewer ist, wie andere Antworten gesagt haben, Pflanzen und hat nichts damit zu tun, ein technisches Interview zu führen.

Das heißt, seine Frage ist gültig. Und die Antwort darauf, wann und wie Sie optimieren, lautet , wenn Sie bewiesen haben, dass es notwendig ist, und Sie haben es profiliert, um genau zu beweisen, welche Teile es benötigen . Knuth sagte bekanntlich, dass vorzeitige Optimierung die Wurzel allen Übels ist, weil es zu einfach ist, unwichtige Abschnitte zu vergolden oder Änderungen (wie die Ihres Interviewers) vorzunehmen, die keine Wirkung haben, während die Stellen fehlen, die sie wirklich brauchen. Bis Sie einen harten Beweis dafür haben, dass dies wirklich notwendig ist, ist die Klarheit des Codes das wichtigere Ziel.

Bearbeiten FabioTurati weist zutreffend darauf hin, dass dies der entgegengesetzte logische Sinn zum Original ist (mein Fehler!) Und dass dies eine weitere Auswirkung von Knuths Zitat darstellt, bei der wir riskieren, den Code zu brechen, während wir es versuchen um es zu optimieren.

33
Graham

Wann ist es angebracht, Methode A gegen Methode B anzuwenden und umgekehrt?

Hardware ist billig; Programmierer sind teuer . Die Kosten für die Zeit, die Sie beide mit dieser Frage verschwendet haben, sind wahrscheinlich weitaus schlimmer als bei beiden Antworten.

Unabhängig davon würden die meisten modernen Compiler einen Weg finden, die lokale Variable in einem Register zu optimieren (anstatt Stapelspeicher zuzuweisen), sodass die Methoden hinsichtlich des ausführbaren Codes wahrscheinlich identisch sind. Aus diesem Grund würden die meisten Entwickler die Option auswählen, die die Absicht am klarsten kommuniziert (siehe Schreiben von wirklich offensichtlichem Code (ROC) ). Meiner Meinung nach wäre das Methode A.

Wenn es sich jedoch um eine rein akademische Übung handelt, können Sie mit Methode C das Beste aus beiden Welten genießen:

private bool IsSumInRange(int a, int b)
{
    a += b;
    return (a >= -1000 && a <= 1000);
}
16
John Wu

Ich würde die Lesbarkeit optimieren. Methode X:

private bool IsSumInRange(int number1, int number2)
{
    return IsValueInRange(number1+number2, -1000, 1000);
}

private bool IsValueInRange(int Value, int Lowerbound, int Upperbound)
{
    return  (Value >= Lowerbound && Value <= Upperbound);
}

Kleine Methoden, die nur eine Sache tun, aber leicht zu überlegen sind.

(Dies ist eine persönliche Präferenz. Ich mag positive Tests anstelle von negativen. Ihr ursprünglicher Code testet tatsächlich, ob der Wert NICHT außerhalb des Bereichs liegt.)

11
Pieter B

Kurz gesagt, ich denke nicht, dass die Frage im aktuellen Computing von großer Relevanz ist, aber aus historischer Sicht ist es eine interessante Gedankenübung.

Ihr Interviewer ist wahrscheinlich ein Fan des Mythical Man Month. In dem Buch macht Fred Brooks den Fall, dass Programmierer im Allgemeinen zwei Versionen von Schlüsselfunktionen in ihrer Toolbox benötigen: eine speicheroptimierte Version und eine CPU-optimierte Version. Fred stützte sich dabei auf seine Erfahrung bei der Entwicklung des IBM System/360-Betriebssystems, bei dem Maschinen möglicherweise nur 8 Kilobyte RAM haben. In solchen Maschinen kann der für lokale Variablen in Funktionen erforderliche Speicher möglicherweise wichtig sein, insbesondere wenn der Compiler sie nicht effektiv optimiert hat (oder wenn Code direkt in Assemblersprache geschrieben wurde).

Ich denke, in der gegenwärtigen Zeit wird es Ihnen schwer fallen, ein System zu finden, bei dem das Vorhandensein oder Fehlen einer lokalen Variablen in einer Methode einen spürbaren Unterschied macht. Damit eine Variable eine Rolle spielt, muss die Methode rekursiv sein, wobei eine tiefe Rekursion erwartet wird. Selbst dann ist es wahrscheinlich, dass die Stapeltiefe überschritten wird, was zu Stapelüberlauf-Ausnahmen führt, bevor die Variable selbst ein Problem verursacht. Das einzige reale Szenario, in dem es sich möglicherweise um ein Problem handelt, besteht darin, dass sehr große Arrays in einer rekursiven Methode auf dem Stapel zugewiesen werden. Das ist aber auch unwahrscheinlich, da ich denke, dass die meisten Entwickler zweimal über unnötige Kopien großer Arrays nachdenken würden.

6
Eric

Nach der Zuordnung s = a + b; Die Variablen a und b werden nicht mehr verwendet. Daher wird für s kein Speicher verwendet, wenn Sie keinen vollständig gehirngeschädigten Compiler verwenden. Speicher, der ohnehin für a und b verwendet wurde, wird wiederverwendet.

Die Optimierung dieser Funktion ist jedoch völliger Unsinn. Wenn Sie Platz sparen könnten, wären es vielleicht 8 Bytes, während die Funktion ausgeführt wird (die wiederhergestellt wird, wenn die Funktion zurückkehrt), also absolut sinnlos. Wenn Sie Zeit sparen könnten, wären es einzelne Nanosekunden. Dies zu optimieren ist reine Zeitverschwendung.

4
gnasher729

Lokale Werttypvariablen werden auf dem Stapel zugewiesen oder verwenden (wahrscheinlicher für solch kleine Codeteile) Register im Prozessor und sehen niemals RAM. In jedem Fall sind sie kurzlebig und kein Grund zur Sorge. Sie ziehen die Verwendung des Speichers in Betracht, wenn Sie Datenelemente in Sammlungen puffern oder in die Warteschlange stellen müssen, die möglicherweise groß und langlebig sind.

Dann kommt es darauf an, was Sie für Ihre Anwendung am meisten interessiert. Verarbeitungsgeschwindigkeit? Reaktionszeit? Speicherbedarf? Wartbarkeit? Konsistenz im Design? Ganz Dir überlassen.

3
Martin Maat

Wie andere Antworten bereits sagten, müssen Sie überlegen, wofür Sie optimieren.

In diesem Beispiel vermute ich, dass jeder anständige Compiler für beide Methoden äquivalenten Code generieren würde, sodass die Entscheidung keine Auswirkungen auf den Laufzeitspeicher oder Speicher hätte!

Was es bewirkt , ist die Lesbarkeit des Codes. (Code kann von Menschen gelesen werden, nicht nur von Computern.) Es gibt keinen allzu großen Unterschied zwischen den beiden Beispielen. Wenn alle anderen Dinge gleich sind, halte ich Kürze für eine Tugend, daher würde ich wahrscheinlich Methode B wählen. Aber alle anderen Dinge sind selten gleich, und in einem komplexeren Fall in der realen Welt könnte dies einen großen Effekt haben.

Dinge, die man beachten muss:

  • Hat der Zwischenausdruck irgendwelche Nebenwirkungen? Wenn es unreine Funktionen aufruft oder Variablen aktualisiert, ist das Duplizieren natürlich eine Frage der Korrektheit und nicht nur des Stils.
  • Wie komplex ist der Zwischenausdruck? Wenn viele Berechnungen durchgeführt und/oder Funktionen aufgerufen werden, kann der Compiler diese möglicherweise nicht optimieren. Dies würde die Leistung beeinträchtigen. (Obwohl, wie Knuth sagte , "wir sollten kleine Wirkungsgrade vergessen, sagen wir ungefähr 97% der Zeit".)
  • Hat die Zwischenvariable eine Bedeutung ? Könnte es einen Namen geben, der erklärt, was los ist? Ein kurzer, aber informativer Name könnte den Code besser erklären, während ein bedeutungsloser nur visuelles Rauschen ist.
  • Wie lang ist der Zwischenausdruck? Wenn es lang ist, kann das Duplizieren den Code länger und schwerer lesbar machen (insbesondere, wenn ein Zeilenumbruch erzwungen wird). Andernfalls könnte die Duplizierung insgesamt kürzer sein.
2
gidds

Wie viele der Antworten gezeigt haben, macht der Versuch, diese Funktion mit modernen Compilern zu optimieren, keinen Unterschied. Ein Optimierer kann höchstwahrscheinlich die beste Lösung finden (stimmen Sie der Antwort zu, die den Assembler-Code zeigt, um dies zu beweisen!). Sie haben angegeben, dass der Code im Interview nicht genau der Code ist, den Sie vergleichen sollen. Vielleicht macht das tatsächliche Beispiel etwas mehr Sinn.

Aber schauen wir uns diese Frage noch einmal an: Dies ist eine Interviewfrage. Das eigentliche Problem ist also, wie sollten Sie darauf antworten, vorausgesetzt, Sie möchten versuchen, den Job zu bekommen?

Nehmen wir auch an, dass der Interviewer weiß, wovon er spricht, und nur versucht, zu sehen, was Sie wissen.

Ich würde erwähnen, dass, wenn man den Optimierer ignoriert, der erste eine temporäre Variable auf dem Stapel erstellen kann, während der zweite dies nicht tun würde, aber die Berechnung zweimal durchführen würde. Daher verwendet der erste mehr Speicher, ist aber schneller.

Sie können auch erwähnen, dass für eine Berechnung möglicherweise eine temporäre Variable erforderlich ist, um das Ergebnis zu speichern (damit es verglichen werden kann). Ob Sie diese Variable benennen oder nicht, spielt also möglicherweise keine Rolle.

Ich würde dann erwähnen, dass in Wirklichkeit der Code optimiert und höchstwahrscheinlich äquivalenter Maschinencode generiert würde, da alle Variablen lokal sind. Es hängt jedoch davon ab, welchen Compiler Sie verwenden (es ist noch nicht lange her, dass ich eine nützliche Leistungsverbesserung erzielen konnte, indem ich eine lokale Variable in Java als "final" deklarierte).

Sie könnten erwähnen, dass der Stapel auf jeden Fall auf seiner eigenen Speicherseite liegt. Wenn Ihre zusätzliche Variable nicht dazu führt, dass der Stapel die Seite überläuft, wird in Wirklichkeit kein Speicher mehr zugewiesen. Wenn es überläuft, wird es eine ganz neue Seite wollen.

Ich würde erwähnen, dass ein realistischeres Beispiel die Wahl sein könnte, ob ein Cache verwendet werden soll, um die Ergebnisse vieler Berechnungen zu speichern, oder nicht, und dies würde eine Frage der CPU gegenüber dem Speicher aufwerfen.

All dies zeigt, dass Sie wissen, wovon Sie sprechen.

Ich würde es bis zum Ende belassen, zu sagen, dass es besser wäre, sich stattdessen auf die Lesbarkeit zu konzentrieren. Obwohl dies in diesem Fall zutrifft, kann es im Interviewkontext als "Ich weiß nichts über Leistung, aber mein Code liest sich wie eine Janet und John Geschichte" interpretiert werden.

Was Sie nicht tun sollten, ist die üblichen langweiligen Aussagen darüber, wie Codeoptimierung nicht erforderlich ist. Optimieren Sie nicht, bis Sie den Code profiliert haben (dies zeigt nur an, dass Sie keinen schlechten Code für sich selbst sehen können). Die Hardware kostet weniger als Programmierer , und bitte, bitte, zitiere Knuth nicht "vorzeitiges bla bla ...".

Die Codeleistung ist in vielen Organisationen ein echtes Problem, und viele Organisationen benötigen Programmierer, die sie verstehen.

Insbesondere bei Organisationen wie Amazon hat ein Teil des Codes eine enorme Hebelwirkung. Ein Code-Snippet kann auf Tausenden von Servern oder Millionen von Geräten bereitgestellt und jeden Tag im Jahr milliardenfach pro Tag aufgerufen werden. Es kann Tausende ähnlicher Schnipsel geben. Der Unterschied zwischen einem schlechten und einem guten Algorithmus kann leicht ein Faktor von tausend sein. Machen Sie die Zahlen und multiplizieren Sie das alles: Es macht einen Unterschied. Die potenziellen Kosten für die Organisation von nicht leistungsfähigem Code können sehr hoch oder sogar schwerwiegend sein, wenn einem System die Kapazität ausgeht.

Darüber hinaus arbeiten viele dieser Organisationen in einem wettbewerbsorientierten Umfeld. Sie können Ihren Kunden also nicht einfach sagen, dass sie einen größeren Computer kaufen sollen, wenn die Software Ihres Konkurrenten auf der vorhandenen Hardware bereits einwandfrei funktioniert oder wenn die Software auf einem Mobiltelefon ausgeführt wird und nicht aktualisiert werden kann. Einige Anwendungen sind besonders leistungskritisch (Spiele und mobile Apps kommen in den Sinn) und können je nach Reaktionsfähigkeit oder Geschwindigkeit leben oder sterben.

Ich persönlich habe über zwei Jahrzehnte an vielen Projekten gearbeitet, bei denen Systeme aufgrund von Leistungsproblemen ausgefallen oder unbrauchbar waren, und ich wurde aufgefordert, diese Systeme zu optimieren, und in allen Fällen war dies auf schlechten Code zurückzuführen, der von Programmierern geschrieben wurde, die dies nicht verstanden haben die Auswirkungen dessen, was sie schrieben. Außerdem ist es nie ein Stück Code, es ist immer überall. Wenn ich auftauche, ist es viel zu spät, über die Leistung nachzudenken: Der Schaden wurde angerichtet.

Das Verstehen der Codeleistung ist eine gute Fähigkeit, die Sie ebenso haben müssen wie das Verstehen der Codekorrektheit und des Codestils. Es kommt aus der Praxis. Leistungsfehler können genauso schlimm sein wie Funktionsfehler. Wenn das System nicht funktioniert, funktioniert es nicht. Egal warum. Ebenso sind Leistung und Funktionen, die niemals verwendet werden, schlecht.

Wenn der Interviewer Sie nach der Leistung fragt, würde ich empfehlen, so viel Wissen wie möglich zu demonstrieren. Wenn die Frage schlecht erscheint, weisen Sie höflich darauf hin, warum Sie der Meinung sind, dass dies in diesem Fall kein Problem darstellt. Zitiere Knuth nicht.

1
rghome

Wann muss der Speicher für eine Methode im Vergleich zur Leistungsgeschwindigkeit optimiert werden?

Nachdem Sie die Funktionalität richtig zuerst erhalten haben. Dann befasst sich Selektivität mit Mikrooptimierungen.


Als Interviewfrage zu Optimierungen provoziert der Code die übliche Diskussion, verfehlt jedoch das übergeordnete Ziel von Ist der Code funktional korrekt?

Sowohl C++ als auch C und andere betrachten den Überlauf int als ein Problem aus dem a + b. Es ist nicht gut definiert und C nennt es undefiniertes Verhalten . Es ist nicht angegeben, um zu "wickeln" - obwohl dies das übliche Verhalten ist.

bool IsSumInRange(int a, int b) {
    int s = a + b;  // Overflow possible
    if (s > 1000 || s < -1000) return false;
    else return true;
}

Es wird erwartet, dass eine solche Funktion mit dem Namen IsSumInRange() gut definiert ist und für alle int -Werte von a,b korrekt ausgeführt wird. Das rohe a + b ist nicht. Eine C-Lösung könnte verwenden:

#define N 1000
bool IsSumInRange_FullRange(int a, int b) {
  if (a >= 0) {
    if (b > INT_MAX - a) return false;
  } else {
    if (b < INT_MIN - a) return false;
  }
  int sum = a + b;
  if (sum > N || sum < -N) return false;
  else return true;
}

Der obige Code könnte optimiert werden, indem ein breiterer Integer-Typ als int verwendet wird, falls verfügbar, wie unten angegeben, oder indem die Tests sum > N, sum < -N innerhalb von if (a >= 0) Logik. Solche Optimierungen führen jedoch möglicherweise nicht wirklich zu "schnellerem" emittiertem Code bei einem intelligenten Compiler und sind auch nicht die zusätzliche Wartung wert, klug zu sein.

  long long sum a;
  sum += b;

Selbst die Verwendung von abs(sum) ist anfällig für Probleme, wenn sum == INT_MIN.

Über welche Art von Compilern sprechen wir und welche Art von "Erinnerung"? Denn in Ihrem Beispiel wird unter der Annahme eines vernünftigen Optimierers der Ausdruck a+b muss vor einer solchen Arithmetik im Allgemeinen in einem Register (einer Form von Speicher) gespeichert werden.

Wenn wir also von einem dummen Compiler sprechen, der auf a+b zweimal werden mehr Register (Speicher) in Ihrem zweiten Beispiel zugewiesen, da Ihr erstes Beispiel diesen Ausdruck möglicherweise nur einmal in einem einzelnen Register speichert, das der lokalen Variablen zugeordnet ist, aber wir sprechen über sehr dumme Compiler an dieser Stelle ... es sei denn, Sie arbeiten mit einer anderen Art von albernem Compiler, der jede einzelne Variable überall verschüttet. In diesem Fall würde vielleicht die erste mehr verursachen Trauer zu optimieren als die zweite *.

Ich möchte das immer noch kratzen und denke, dass der zweite wahrscheinlich mehr Speicher mit einem dummen Compiler verbraucht, selbst wenn er dazu neigt, Verschüttungen zu stapeln, weil er möglicherweise drei Register für a+b und verschütten a und b mehr. Wenn es sich um den primitivsten Optimierer handelt, erfassen Sie a+b to s wird wahrscheinlich "helfen", weniger Register/Stapelverschüttungen zu verwenden.

Dies alles ist auf ziemlich dumme Weise äußerst spekulativ, wenn keine Messungen/Demontagen vorgenommen werden, und selbst im schlimmsten Fall handelt es sich nicht um einen "Speicher vs. Leistung" -Fall (denn selbst unter den schlechtesten Optimierern, die ich mir vorstellen kann, sprechen wir nicht Über alles andere als temporären Speicher (wie Stapel/Register) ist es bestenfalls ein "Leistungs" -Fall, und unter jedem vernünftigen Optimierer sind die beiden gleichwertig, und wenn man keinen vernünftigen Optimierer verwendet, warum ist man dann von einer so mikroskopischen Optimierung besessen? besonders fehlende Messungen? Das ist wie Befehlsauswahl/Registerzuordnung Fokus auf Baugruppenebene, von dem ich niemals erwarten würde, dass jemand, der produktiv bleibt, wenn er beispielsweise einen Interpreter verwendet, der alles verschüttet.

Wann muss der Speicher für eine Methode im Vergleich zur Leistungsgeschwindigkeit optimiert werden?

Was diese Frage betrifft, wenn ich sie breiter angehen kann, finde ich die beiden oft nicht diametral entgegengesetzt. Insbesondere wenn Ihre Zugriffsmuster sequentiell sind und die Geschwindigkeit des CPU-Cache gegeben ist, führt eine Reduzierung der Anzahl der nacheinander für nicht triviale Eingaben verarbeiteten Bytes (bis zu einem gewissen Punkt) dazu, dass diese Daten schneller durchforstet werden. Natürlich gibt es Bruchstellen, an denen es schneller sein kann, nacheinander in größerer Form zu verarbeiten, wenn die Daten im Austausch gegen viel, viel mehr Anweisungen viel, viel kleiner sind, um weniger Anweisungen zu erhalten.

Ich habe jedoch festgestellt, dass viele Entwickler dazu neigen, zu unterschätzen, inwieweit eine Reduzierung der Speichernutzung in solchen Fällen zu einer proportionalen Reduzierung der Verarbeitungszeit führen kann. Es ist sehr menschlich intuitiv, die Leistungskosten in Anweisungen und nicht in den Speicherzugriff zu übersetzen, um nach großen LUTs zu greifen, und zwar in einem vergeblichen Versuch, einige kleine Berechnungen zu beschleunigen, nur um festzustellen, dass die Leistung durch den zusätzlichen Speicherzugriff beeinträchtigt wird.

Bei Fällen mit sequentiellem Zugriff über ein großes Array (ohne lokale skalare Variablen wie in Ihrem Beispiel) befolge ich die Regel, dass weniger Speicher zum sequentiellen Durchpflügen zu einer höheren Leistung führt, insbesondere wenn der resultierende Code einfacher als sonst ist, bis dies nicht der Fall ist Bis meine Messungen und mein Profiler mir etwas anderes sagen und es wichtig ist, gehe ich davon aus, dass das sequentielle Lesen einer kleineren Binärdatei auf der Festplatte schneller durchzuarbeiten ist als eine größere (selbst wenn die kleinere mehr Anweisungen erfordert ), bis gezeigt wird, dass diese Annahme in meinen Messungen nicht mehr gilt.

0
Dragon Energy

Sie sollten zuerst die Richtigkeit optimieren.

Ihre Funktion schlägt für Eingabewerte fehl, die nahe an Int.MaxValue liegen:

int a = int.MaxValue - 200;
int b = int.MaxValue - 200;
bool inRange = test.IsSumInRangeA(a, b);

Dies gibt true zurück, da die Summe auf -400 überläuft. Die Funktion funktioniert auch nicht für a = int.MinValue + 200. (summiert sich fälschlicherweise zu "400")

Wir werden nicht wissen, wonach der Interviewer gesucht hat, es sei denn, er oder sie mischt sich ein, aber "Überlauf ist real".

Stellen Sie in einer Interview-Situation Fragen, um den Umfang des Problems zu klären: Was sind die zulässigen maximalen und minimalen Eingabewerte? Sobald Sie diese haben, können Sie eine Ausnahme auslösen, wenn der Anrufer Werte außerhalb des Bereichs übermittelt. Oder (in C #) können Sie einen markierten Abschnitt {} verwenden, der beim Überlauf eine Ausnahme auslöst. Ja, es ist arbeitsintensiver und komplizierter, aber manchmal ist es das, was es braucht.

0
TomEberhard

Ihre Frage hätte lauten sollen: "Muss ich das überhaupt optimieren?".

Version A und B unterscheiden sich in einem wichtigen Detail, das A vorzuziehen macht, aber es hängt nicht mit der Optimierung zusammen: Sie wiederholen keinen Code.

Die eigentliche "Optimierung" wird als allgemeine Eliminierung von Unterausdrücken bezeichnet, was so ziemlich jeder Compiler tut. Einige führen diese grundlegende Optimierung auch dann durch, wenn die Optimierungen deaktiviert sind. Das ist also keine wirkliche Optimierung (der generierte Code wird mit ziemlicher Sicherheit in jedem Fall genau gleich sein).

Aber wenn es nicht eine Optimierung ist, warum ist es dann vorzuziehen? Okay, du wiederholst keinen Code, wen interessiert das?

Zunächst einmal haben Sie nicht das Risiko, versehentlich die Hälfte der Bedingungsklausel falsch zu verstehen. Aber was noch wichtiger ist, jemand, der diesen Code liest, kann sofort was Sie versuchen zu tun, anstelle einer if((((wtf||is||this||longexpression)))) Erfahrung. Was der Leser zu sehen bekommt, ist if(one || theother), was eine gute Sache ist. Es kommt nicht selten vor, dass Sie diese andere Person drei Jahre später Ihren eigenen Code liest und denkt "WTF bedeutet das?". In diesem Fall ist es immer hilfreich, wenn Ihr Code sofort die Absicht mitteilt. Wenn ein allgemeiner Unterausdruck richtig benannt wird, ist dies der Fall.
Wenn Sie zu irgendeinem Zeitpunkt in der Zukunft entscheiden, dass z. du musst dich ändern a+b bis a-b, du musst eins Ort ändern, nicht zwei. Und es besteht keine Gefahr, dass (erneut) der zweite versehentlich falsch liegt.

Über Ihre eigentliche Frage, wofür Sie optimieren sollten, sollte Ihr Code zunächst richtig sein. Dies ist das absolut Wichtigste. Code, der nicht korrekt ist, ist schlechter Code, auch mehr, wenn er trotz seiner Falschheit "gut funktioniert" oder zumindest sieht aus wie gut funktioniert. Danach sollte der Code lesbar sein (lesbar für jemanden, der mit ihm nicht vertraut ist).
] Algorithmus für das Problem, nicht der am wenigsten effiziente).

Aber für die meisten Anwendungen ist die Leistung, die Sie nach dem Ausführen von korrektem, lesbarem Code unter Verwendung eines vernünftigen Algorithmus durch einen optimierenden Compiler erhalten, meistens in Ordnung. Sie müssen sich keine Sorgen machen.

Wenn dies nicht der Fall ist, d. H. Wenn die Leistung der Anwendung tatsächlich nicht den Anforderungen entspricht, und nur dann, sollten Sie sich Gedanken über lokale Optimierungen machen, wie Sie sie versucht haben. Am besten ist es jedoch, wenn Sie den Algorithmus der obersten Ebene überdenken. Wenn Sie eine Funktion aufgrund eines besseren Algorithmus 500-mal statt 50.000-mal aufrufen, hat dies größere Auswirkungen als die Einsparung von drei Taktzyklen bei einer Mikrooptimierung. Wenn Sie nicht für mehrere hundert Zyklen bei einem zufälligen Speicherzugriff die ganze Zeit stehen bleiben, hat dies eine größere Auswirkung als ein paar billige zusätzliche Berechnungen usw. usw.

Die Optimierung ist eine schwierige Angelegenheit (Sie können ganze Bücher darüber schreiben und kein Ende finden), und es ist normalerweise Zeitverschwendung, Zeit damit zu verbringen, einen bestimmten Punkt blind zu optimieren (ohne zu wissen, ob dies überhaupt der Engpass ist!). Ohne Profiling ist die Optimierung sehr schwer zu erreichen.

Aber als Faustregel, wenn Sie blind fliegen und nur etwas oder als allgemeine Standardstrategie tun müssen/wollen, würde ich vorschlagen, für "Gedächtnis" zu optimieren.
Die Optimierung auf "Speicher" (insbesondere räumliche Lokalität und Zugriffsmuster) bringt normalerweise einen Vorteil, da im Gegensatz zu früher, als alles "irgendwie gleich" war, heutzutage auf RAM) zugegriffen wird gehört zu den teuersten Dingen (kurz vor dem Lesen von der Festplatte!), die Sie im Prinzip tun können. Während ALU dagegen billig ist und jede Woche schneller wird. Die Speicherbandbreite und die Latenz verbessern sich nicht annähernd so schnell. Gut Lokalität und gute Zugriffsmuster können leicht einen 5-fachen Unterschied (20-fache in extremen, erfundenen Beispielen) in der Laufzeit im Vergleich zu schlechten Zugriffsmustern in datenintensiven Anwendungen bewirken. Seien Sie nett zu Ihren Caches, und Sie werden eine glückliche Person sein.

Überlegen Sie sich, was die verschiedenen Dinge, die Sie tun können, kosten, um den vorherigen Absatz ins rechte Licht zu rücken. So etwas wie a+b dauert (wenn nicht optimiert) ein oder zwei Zyklen, aber die CPU kann normalerweise mehrere Befehle pro Zyklus starten und nicht abhängige Befehle so realistisch weiterleiten, dass Sie nur etwa einen halben Zyklus oder weniger kosten. Wenn der Compiler gut terminiert und je nach Situation gut ist, kostet er im Idealfall null.
Das Abrufen von Daten ("Speicher") kostet Sie entweder 4-5 Zyklen, wenn Sie Glück haben und es in L1 ist, und ungefähr 15 Zyklen, wenn Sie nicht so viel Glück haben (L2-Treffer). Wenn sich die Daten überhaupt nicht im Cache befinden, dauert es mehrere hundert Zyklen. Wenn Ihr zufälliges Zugriffsmuster die Funktionen des TLB überschreitet (einfach mit nur ~ 50 Einträgen), fügen Sie weitere hundert Zyklen hinzu. Wenn Ihr zufälliges Zugriffsmuster tatsächlich einen Seitenfehler verursacht, kostet es Sie im besten Fall einige zehntausend Zyklen und im schlimmsten Fall mehrere Millionen.
Denken Sie jetzt darüber nach, was möchten Sie am dringendsten vermeiden?

0
Damon