LOOP ... INTO ist schneller als LOOP ... ASSIGNING !

LOOP ... INTO ist schneller als LOOP ... ASSIGNING !

Veröffentlicht am 30. Mai 2023 von

Jörg Brandeis

| ABAP | BW/4HANA | S/4HANA |

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:

  1. Nichts - Direkt im LOOP ein CHECK der den Durchgang abbricht
  2. Aufruf einer leeren Methode
  3. 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...
  4. 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:

  1. LOOP ... INTO wa - Entspricht einer Schleife in der die aktuelle Zeile bei jeder Iteration in die Workarea wa kopiert wird.
  2. 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.
  3. LOOP ... REFERENCE INTO ref - Erstellt ebenfalls keine Kopie der Daten. Statt dessen wird eine Referenz auf die Zeile in der Tabelle in das Feld ref 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\ComplexityEmpty, just CheckEmpty Method CallSome Logic in MethodsSome Logic
INTO3 ms15 ms109 ms74 ms
ASSIGNING2 ms20 ms147 ms87 ms
REFERENCE2 ms22 ms151 ms93 ms

Breite Struktur - viele Bytes:

22 Felder, 2518 Bytes

Approach\ComplexityEmpty, just CheckEmpty Method CallSome Logic in MethodsSome Logic
INTO21 ms32 ms128 ms91 ms
ASSIGNING2 ms21 ms153 ms91 ms
REFERENCE2 ms22 ms155 ms97 ms

Breite Struktur - viele Felder:

50 Felder, 996 bytes

Approach\ComplexityEmpty, just CheckEmpty Method CallSome Logic in MethodsSome Logic
INTO7 ms18 ms110 ms76 ms
ASSIGNING2 ms21 ms152 ms91 ms
REFERENCE2 ms22 ms156 ms96 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:

DatentypZugriffszeit
Struktur106 ms
Feldsymbol106 ms
Referenz108 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 im LOOP mit unpassendem Tabellentyp oder Schlüssel
  • SELECT im LOOP
  • 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...


  1. 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.
  2. In diesem Artikel schreibe ich vereinfachtend LOOP ... ASSIGNING, das schliesst stets LOOP ... REFERENCE INTO ein, soweit ich nicht weiter differenziert habe. Denn beide Varianten haben ein sehr ähnliches Verhalten.
  3. 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
  4. 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.