Eine Frage so alt wie die ABAP Programmierung1: Was ist besser? LOOP .. INTO, LOOP .. ASSIGNING oder LOOP ... REFERENCE INTO2? 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
LOOPeinCHECKder den Durchgang abbricht - Aufruf einer leeren Methode
- Etwas sinnlose Logik, verteilt auf 4 Methoden: Zwei Komponenten der Struktur Assignen, Eine Berechnung mit
DATSWerten 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 Workareawakopiert 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 Feldrefgeschrieben.
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
LOOPfast 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:
READimLOOPmit unpassendem Tabellentyp oder SchlüsselSELECTimLOOP- 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...
Footnotes
-
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 INTOein, 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. ↩




