LOOP ... INTO ist schneller als LOOP ... ASSIGNING !
Veröffentlicht am 30. Mai 2023 von | ABAP | BW/4HANA | S/4HANA |
Eine Frage so alt wie die ABAP Programmierung1: Was ist besser? LOOP .. INTO
, LOOP .. ASSIGNING
oder LOOP ... REFERENCE INTO
2? In der Diskussion darüber spielt die Laufzeit häufig eine große Rolle. Oft wird aber vergessen, das die Schleife noch eine Schleifenkörper hat, der bei jedem einzelnen Durchlauf ausgeführt wird. Und das es um die Gesamtlaufzeit geht. Darum habe ich für diesen Blogpost ein paar Messungen gemacht. Und dabei bin ich zu einem überraschenden Ergebnis kommen. Denn LOOP ... INTO
ist oft die schnellste Variante!
Der Fokus in dem Artikel ist auf lesenden Zugriffen. Wenn die Daten in der Internen Tabelle geändert werden, dann ist ASSIGNING
unumstritten die bessere Wahl. Andere Aspekte, wie zum Beispiel die Lesbarkeit oder Clean Code habe ich in diesem Artikel ausgelassen. Damit beschäftige ich mich in meinem nächsten Artikel.
Die Frage LOOP
oder ASSIGNING
taucht in fast jeder meiner Schulungen über modernes ABAP oder Clean ABAP auf, aber manchmal auch in Projekten. Die verwendeten Argumente sind zum Teil nicht ganz korrekt und die Diskussion wird oft sehr dogmatisch geführt. Darum wollte ich die Diskussion etwas objektivieren und mein wichtigstes Argument belegen: Der Schleifenkörper ist dominant, die Variante spielt deshalb eine untergeordnete Rolle. Das ich sogar feststelle, das INTO
oft besser ist, damit hatte ich nicht gerechnet.
Überblick über den Vergleich
Eine leere Schleife ist sinnlos. Trotzdem spielt sie die Grundlagen von so manchem Kommentar oder Artikel.3 Damit uns das nicht passiert, brauchen wir irgend etwas, das in dem Schleifenkörper passiert. Das habe ich in die Methode DO_SOMETHING
ausgelagert. Im Abschnitt Komplexität habe ich die Szenarien beschrieben, die durch den Parameter Compexity
gesteuert werden.
DO 3 TIMES.
DATA(lv_complexity) = sy-index.
LOOP AT mt_data INTO DATA(ls_data).
CHECK lv_complexity > 1.
do_something( Complexity = lv_Complexity
is_data = ls_data ).
ENDLOOP.
ENDDO.
Und weil ich bei den Tests festgestellt habe, dass auch der Methodenaufruf signifikanten Einfluss auf die Laufzeit hat, habe ich zusätzlich noch mal ein 4. Szenario getestet mit der gleichen Komplexität ohne Methodenaufruf.
Die Dimensionen
- Breite der Tabelle/Struktur
- Komplexität im Schleifenkörper
- Anzahl der Datensätze
- LOOP-Varianten
Im Detail werden diese im Folgenden besprochen.
Die Breite der Struktur
- Schmal (6 Felder, 54 Bytes)
- Viele Bytes (22 Felder, 2518 Bytes)
- Viele Felder (50 Felder, 996 Bytes)
Komplexität
Wir haben hier in den Testreien durchweg eine geringe Komplexität. Die Aufgaben die in diesen Beispielen gemacht werden, sind erheblich geringer als die Schleifenkörper in den allermeisten LOOP
Schleifen in der freien Wildbahn. In der Praxis habe ich schon Schleifen gesehen, die weit über 2000 Zeilen Code enthalten. Hier unsere vier Szenarien:
- Nichts - Direkt im
LOOP
einCHECK
der den Durchgang abbricht - Aufruf einer leeren Methode
- Etwas sinnlose Logik, verteilt auf 4 Methoden: Zwei Komponenten der Struktur Assignen, Eine Berechnung mit
DATS
Werten mit Fuba Aufruf, zwei Vergleiche in einer IF-Verzweigung. Siehe Code rechts... - Die gleiche sinnlose Logik direkt, ohne Methoden drum rum. Dieser Testfall wurde nachträglich eingebaut und ist im Code rechts nicht zu erkennen. Grund hierfür war, dass wir beobachtet haben, dass Methodenaufrufe ungünstig sind. Um das zu quantifizieren haben wir auch einen Testfall mit Komplexität aber ganz ohne Methodenaufruf gebaut.
Anzahl Datensätze
Die Anzahl der Datensätze wurde testweise variiert. Die Laufzeiten haben sich erwartungsgemäß für alle Szenarien linear zur Anzahl der Datensätze entwickelt. Wir haben die Anzahl der Datensätze darum fix auf 50000 gesetzt. Das entspricht der typischen Paketgröße eines DTPs im SAP BW bei ABAP Ausführung.
METHOD do_something.
CHECK Complexity > 1.
do_nothing( ).
CHECK Complexity > 2.
do_assign_components( is_data ).
do_date_things( is_data ).
do_comparisons( is_data ).
ENDMETHOD.
METHOD do_assign_components.
ASSIGN COMPONENT 'BUDAT'
OF STRUCTURE is_data
TO FIELD-SYMBOL(<date>).
ASSIGN COMPONENT 'SUMMARY'
OF STRUCTURE is_data
TO FIELD-SYMBOL(<field>).
ENDMETHOD.
METHOD do_date_things.
DATA day TYPE cind.
DATA(Tomorrow) = is_data-created_on .
Tomorrow = Tomorrow + 1.
CALL FUNCTION 'DATE_COMPUTE_DAY'
EXPORTING date = Tomorrow
IMPORTING day = Day.
DATA(DaysUntilTomorrow) = Tomorrow - sy-datum.
ENDMETHOD.
METHOD do_comparisons.
IF is_data-title = 'Morbi vel '.
ENDIF.
IF is_data-title CA 'ABC'.
ENDIF.
ENDMETHOD.
LOOP Varianten
Die LOOP-Variante ist das eigentliche Forschungsobjekt. Kurz zusammengefasst beschreibe ich sie hier noch einmal, auch wenn die meiste Leser das schon wissen:
LOOP ... INTO wa
- Entspricht einer Schleife in der die aktuelle Zeile bei jeder Iteration in die Workareawa
kopiert wird.LOOP ... ASSIGNING <fs>
- Erstellt keine Kopie der Daten. Statt dessen wird nur das Feldsymbol<fs>
darauf zeigen. Diese Variante ist insbesondere dann vorteilhaft, wenn die Daten der Tabelle geändert werden sollen. Denn das Feldsymbol zeigt auf die Daten der Tabelle.LOOP ... REFERENCE INTO ref
- Erstellt ebenfalls keine Kopie der Daten. Statt dessen wird eine Referenz auf die Zeile in der Tabelle in das Feldref
geschrieben.
LOOP ... INTO
LOOP AT mt_data INTO ls_data.
CHECK lv_complexity > 1.
do_something(
Complexity = lv_Complexity
is_data = ls_data ).
ENDLOOP.
LOOP ... ASSIGNING
LOOP AT mt_data ASSIGNING <ls_data>.
CHECK lv_complexity > 1.
do_something(
Complexity = lv_Complexity
is_data = <ls_data> ).
ENDLOOP.
LOOP ... REFERENCE INTO
LOOP AT mt_data REFERENCE
INTO lr_data.
CHECK lv_complexity > 1.
do_something(
Complexity = lv_Complexity
is_data = lr_data->* ).
ENDLOOP.
Messungen
Schmale Struktur
6 Felder, 54 Bytes
Approach\Complexity | Empty, just Check | Empty Method Call | Some Logic in Methods | Some Logic |
---|---|---|---|---|
INTO | 3 ms | 15 ms | 109 ms | 74 ms |
ASSIGNING | 2 ms | 20 ms | 147 ms | 87 ms |
REFERENCE | 2 ms | 22 ms | 151 ms | 93 ms |
Breite Struktur - viele Bytes:
22 Felder, 2518 Bytes
Approach\Complexity | Empty, just Check | Empty Method Call | Some Logic in Methods | Some Logic |
---|---|---|---|---|
INTO | 21 ms | 32 ms | 128 ms | 91 ms |
ASSIGNING | 2 ms | 21 ms | 153 ms | 91 ms |
REFERENCE | 2 ms | 22 ms | 155 ms | 97 ms |
Breite Struktur - viele Felder:
50 Felder, 996 bytes
Approach\Complexity | Empty, just Check | Empty Method Call | Some Logic in Methods | Some Logic |
---|---|---|---|---|
INTO | 7 ms | 18 ms | 110 ms | 76 ms |
ASSIGNING | 2 ms | 21 ms | 152 ms | 91 ms |
REFERENCE | 2 ms | 22 ms | 156 ms | 96 ms |
Beobachtungen
Stabile Ausführungszeiten
Bei allen Ausführungen waren die Laufzeiten relativ konstant und alle Ergebnisse reproduzierbar.
Konstante Laufzeiten bei INTO REFERENCE
und ASSIGNING
Bei den beiden Ansätzen gibt es keine Abhängigkeiten von der Breite der Struktur. Das ist erwartbar, denn die Daten werden nicht kopiert.
Laufzeiten von LOOP ... INTO
hängen von der Breite der Struktur ab
Genaugenommen linear zur Breite in Byte zu sein. Die Anzahl der Felder hingegen hat offenbar keinen Einfluss. In unseren Beispielen geht das bis 2.5 kb. Eine EKPO
bringt aber auch fast 6kb Breite auf die Waage. Da ist schon zweifelhaft, ob man die ganze Pracht braucht oder ob ein Ausschnitt nicht auch genügt hätte. Wir wollten SELECT *
ja vermeiden, oder? Zumindest wenn wir ernsthaft über Performance reden.
Variante LOOP ... ASSIGNING
ist stets schneller als LOOP ... REFERENCE INTO
Diese Aussage ist schon oft diskutiert und belegt worden. Sie lässt sich aber auch an unseren Tests gut nachvollziehen. Der Unterschied ist aber marginal.
Verhältnis Schleife zu Schleifenkörper.
Selbst wenn die Schleife mit leerem Schleifenkörper im ungünstigsten Falle mit einer sehr breiten Struktur bei LOOP ... INTO
maximal 21 ms benötigt, ist auch für kleinste Aufgaben der Schleifenkörper dominant. Schon der Aufruf einer leeren Methode dauert ca. 10 ms. Mit kleinen Berechnungen oder Vergleichen sind wir schnell bei 100ms.
==> Nicht die Schleife ist langsam, sondern das was in der Schleife passiert. Weil es eben X-Mal ausgeführt wird.
Der Zugriff auf Feldsymbole und Referenzen in LOOP
-Schleifen ist langsamer
In allen Messungen habe ich beobachtet, dass die Verwendung von Feldsymbolen oder Referenzen auf eine aktuelle Zeile der Internen Tabelle langsamer ist als eine Struktur. Das gilt insbesondere für Methodenaufrufe. Bei der Variante 4 sind das sogar 40 ms. Selbst mit einer sehr breiten Struktur ist INTO
schneller.
Mir ist das nicht erklärlich. Über Erklärungsversuche oder Gegenbeweise freue ich mich.
Eine Gegenprobe mit einer DO
-Schleife hat gezeigt, dass dieses Phänomen nur im LOOP
auftritt. Im Normalfall sind Strukturen, Feldsymbole und Referenzen von der Zugriffsgeschwindigkeit nahezu identisch. Für die gleiche Logik mit der Komplexität 4 habe ich die folgenden Werte gemessen:
Datentyp | Zugriffszeit |
---|---|
Struktur | 106 ms |
Feldsymbol | 106 ms |
Referenz | 108 ms |
Bei nicht-leeren Schleifenkörpern ist LOOP ... INTO
am schnellsten
Diese Beobachtung ist bemerkenswert. Denn sie wiederspricht der landläufigen Meinung, dass LOOP ... ASSIGNING
oder LOOP ... INTO REFERENCE
schneller ist. ASSIGNING
hat nur dann die Nase vorne, wenn die folgenden zwei Bedingungen zusammenkommen:
- Die Struktur ist sehr breit und
- Es findet im
LOOP
fast keine Logik statt
Schlussfolgerung und Bewertung
Die pauschale Aussage: LOOP ... ASSIGNING
ist am schnellsten ist falsch. Sobald man es mit echten Anforderungen zu tun hat, ist LOOP ... INTO
faktisch schneller. Zumindest solange die Breite der Tabelle in einem vernünftigen Maß ist. Aber sehr breite interne Tabellen sollen ja praktisch nicht mehr genutzt werden4. Und Schleifen mit leerem Schleifenkörper sind sinnlos.
Wichtiger ist aber die Erkenntnis, das die LOOP
-Variante eigentlich kaum relevanten Einfluss auf die Laufzeit hat. Denn der Schleifenkörper ist fast immer dominant. Performanceprobleme kommen in der Praxis nich daher, dass jemand die falsche LOOP
-Variante wählt. Denn die Zeitkomplexität ist hier stets O(n). Die meisten Performanceprobleme, die ich in meiner Karriere analysiert habe, hatten aber eine Zeitkomplexität von O(n²). Typische Kandidaten sind zum Beispiel:
READ
imLOOP
mit unpassendem Tabellentyp oder SchlüsselSELECT
imLOOP
- Eine ungünstige Programmstruktur
Gerade weil das Ergebnis mit den schnelleren LOOP ... INTO
unerwartet ist, würde ich mich über Feedback freuen. Vielleicht hat jemand eine passabele Erklärung dafür? Irgend ewtas muss ja im Hintergrund noch passieren, das nicht offensichtlich ist. Denn der reine LOOP
ohne Datenverarbeitung ist ja bei ASSIGNING
schneller. Und warum sind gerade Methodenaufrufe ungünstig? Fragen über Fragen...
- Die Behauptung "Eine Frage so alt wie die ABAP Programmierung" ist wahrscheinlich falsch, da ich nicht davon ausgehen kann, das Feldsymbole in der ersten ABAP Version enthalten waren. Aber die ältesten Releasenotes über ABAP Versionen die ich finden konnte beziehen sich auf die Version 3.0 und dort sind die Feldsymbole schon vorhanden. Siehe z.B. diesen Hinweis aus der ABAP Doku 7.51. Trotzdem eine schöne Einleitung. ↩
- In diesem Artikel schreibe ich vereinfachtend
LOOP ... ASSIGNING
, das schliesst stetsLOOP ... REFERENCE INTO
ein, soweit ich nicht weiter differenziert habe. Denn beide Varianten haben ein sehr ähnliches Verhalten. ↩ - Ein Beispiel für leere Schleifen mit eindeutigem Ergbnis ist in dieser Diskussion zu finden: https://answers.sap.com/questions/7873994/performance-with-field-symbols.html↩
- Da man nicht mehr mit
SELECT *
die gesammte Breite einer DB-Tabelle lesen sollte, sind auch zugehörige interne Tabellen mit der maximalen Breite nicht nötig. Man sollte im Interesse des Clean Codes stets nur die Felder in einer Strutkur bzw. internen Tabelle haben, die für die aktuelle Aufgabe auch relevant sind. Der Bezug auf bestehende, extrem breite DDic-Objekte (DB-Tabellen, Strukturen oder Views) ist zwar bequem, er erschwert aber die Lesbarkeit. Gleiches gilt auch, wenn man sich auf die modernen CDS-Views des VDM bezieht. ↩