ABAP 嵌套使用循环的优化之道

381 阅读6分钟

糟糕的代码

代码 1:

LOOP AT INTERNAL_TABLE.
  IF SOMETHING.
  
    GET_SOME_DATA USING INTERNAL_TABLE.
    
    IF SOMETHING_ELSE.
    
      GET_SOME_MORE_DATA USING INTERNAL_TABLE.

      IF SOMETHING_ELSE_YET_AGAIN.

        DO_SOMETHING.

      ENDIF.

    ENDIF.

  ENDIF.

ENDLOOP.

代码 2:

LOOP AT INTERNAL_TABLE WHERE SOMETHING = TRUE.

   GET_SOME_DATA USING INTERNAL_TABLE.

   CHECK SOMETHING_ELSE = TRUE.

   GET_SOME_MORE_DATA USING INTERNAL_TABLE.

   CHECK SOMETHING_ELSE_YET_AGAIN = TRUE.
   
   DO_SOMETHING.
   
ENDLOOP.

这两个例程在功能上是相同的;只是第二个例程的行数较少,所以如果上下翻页的次数较少是重要的,而我实际上倾向于认为是重要的,那么你已经让普通的浏览者在一个屏幕上看到更多的例程了。

然而,GitHub 上的 SAP 整洁代码指南说不要在循环内使用 CHECK 语句。这是一个摇摆不定的例子--在循环中使用 IF 语句而不是 CHECK 语句会使代码变长......但它是否会使代码更清晰?

在前面的例子中,你会发现我减少了 IF/ENDIF 结构的数量。有深度嵌套的控制块(IF、CASE等)的编程术语是循环复杂性。

在 ABAP 编程指南一书中,其中一条规则是将 IF 语句等控制块的嵌套级别限制在 5 级。这是非常明智的建议--我曾一次又一次地看到巨大的 IF 语句块,深度嵌套,以至于当你做 "漂亮打印 "时,中间的部分会一直缩进到屏幕的最右边。

在这种情况下,我有一种难以忍受的冲动,想把 IFENDIF 之间的部分封装成自己的程序。这可以把那些简直无法理解的东西变成明显的东西。

代码 2 的 IF 语句嵌套的太深了。在我开始研究这段代码之前,每条 ENDIF 后面甚至没有任何注释。我加入了注释,作为简化代码的第一步;也就是说,在你改变它之前,你需要理解它。

在开发过程中,我们经常会遇到需要写双层循环的情况:

SORT gt_out ASCENDING by field1.
LOOP AT gt_out WHERE NOT field1 IS INITIAL.
    LOOP AT gt_itab WHERE field1 = gt_out-field1.
        APPEND gt_itab to gt_subtotal.
    ENDLOOP.
ENDLOOP.

优化思路:

  1. 内表 gt_out 的循环必须处理所有的行。没有什么可改进的,因为它将与表 1 中的行数成线性比例,也就是与 N1 成线性比例。很明显,上面的代码示例循环处理表 gt_itab 中的所有行,即 N2。嵌套循环的总规模为 N1 * N2 。如果 N1 和 N2 都随着处理的数据量 N 而增长,可以是位置数,那么嵌套循环的规模与 N2N^2 成正比,也就是说,它的规模是二次或非线性的。

  2. 因为 gt_itab 中只有一行或几行与 gt_out 中的一行相对应。这些行必须被有效地找到,以便在 N2 上获得比线性更快地缩放行为。

LOOP AT gt_out INTO gs_out.

 Efficient Operation to find the line 
   or lines on table gt_itab which correspond to wa_itab
 
ENDLOOP.

最快的访问类型是当被搜索的行可以被直接访问时,这使得这种读取类型与内部表的大小无关。这可以通过以下方式实现:

  • READ itab WITH index
  • READ sort_tab' WITH index
  • READ hash_tab WITH TABLE KEY ...

通过把其中一个 LOOP 转化为 READ TABLE 语句:

SORT: gt_out BY field1 field2,
      gt_itab BY field1 field2.
      
LOOP AT gt_out.
  READ TABLE gt_itab INTO wa_itab.
    WITH KEY field1 = gt_out-field1
             field2 = gt_out-field2
    BINARY SEARCH.
    
   IF sy-subrc = 0.
      DO_SOMETHING.
   ENDIF.
ENDLOOP.

为了使嵌套循环处理的完整性,有必要对作为内部操作的循环进行说明。如果内表 2 的几行能满足关键条件,就需要一个循环。循环可以通过以下方式实现

  • LOOP AT itab WHERE ...
  • LOOP AT sort_tab WHERE
  • READ itab WITH KEY ... BINARY SEARCH, Save index, LOOP AT itab FROM index, Exit if condition is not fulfilled anymore.

为了更加精确,我们添加了以下代码:

SORT itab2 BY KEY.
LOOP AT gt_out INTO gs_out.
    READ TABLE itab2 TRANSPORTING NO FIELDS WITH KEY key = gs_out-key
    BINARY SEARCH.
    
    tabix2 = sy-tabix.
    
    LOOP AT itab2 INTO wa2 FROM tabix2.
    
    IF ( wa2-key NE wa1-key ).
        EXIT.
    ENDIF.
    
    " DO SOMETHING
    
ENDLOOP.

重要的是,表 2 上的 SORT 是在表 2 的 LOOP 之外,这样它就只被执行一次。二进制搜索可以有效地找到循环的起点,但同样重要的是,一旦条件不再满足,就立即离开循环。

对于标准表的 LOOP AT WHERE ,我们希望对表 2 的大小有一个线性依赖,因为它必须扫描整个表。其他两种方法应该显示出对数的依赖性。

特别是不太为人所知的标准表的变通方法可以帮助改进现有的程序,因为在这些程序中,表的类型已经不可能改变了。

以下是一些示例代码,演示如何优化 ABAP 中的双层 loop 循环:

  1. 避免在循环内部进行数据库访问:
* 不优化的代码
LOOP AT itab1 INTO wa1.
  SELECT * FROM table2 INTO TABLE itab2 WHERE field = wa1-field.
  LOOP AT itab2 INTO wa2.
    " 在这里进行操作
  ENDLOOP.
ENDLOOP.

* 优化的代码
SELECT * FROM table2 INTO TABLE itab2.
LOOP AT itab1 INTO wa1.
  LOOP AT itab2 INTO wa2 WHERE field = wa1-field.
    " 在这里进行操作
  ENDLOOP.
ENDLOOP.
  1. 使用内部表:
* 不优化的代码
LOOP AT itab1 INTO wa1.
  LOOP AT itab2 INTO wa2.
    " 在这里进行操作
  ENDLOOP.
ENDLOOP.

* 优化的代码
DATA: itab3 TYPE TABLE OF ty_itab2.
APPEND LINES OF itab2 TO itab3.
LOOP AT itab1 INTO wa1.
  LOOP AT itab3 INTO wa2 WHERE field = wa1-field.
    " 在这里进行操作
  ENDLOOP.
ENDLOOP.
  1. 使用并行循环:
* 不优化的代码
LOOP AT itab1 INTO wa1.
  LOOP AT itab2 INTO wa2.
    " 在这里进行操作
  ENDLOOP.
ENDLOOP.

* 优化的代码
DATA: itab3 TYPE TABLE OF ty_itab2.
APPEND LINES OF itab2 TO itab3.
PARALLEL CURSOR WITH 8 FIELDS OF TABLE itab1 AS wa1.
  LOOP AT itab3 INTO wa2 WHERE field = wa1-field.
    " 在这里进行操作
  ENDLOOP.
ENDPARALLEL.
  1. 避免在循环内部进行字符串操作:
* 不优化的代码
LOOP AT itab1 INTO wa1.
  CONCATENATE wa1-field 'suffix' INTO lv_string.
  " 在这里进行操作
ENDLOOP.

* 优化的代码
lv_suffix = 'suffix'.
LOOP AT itab1 INTO wa1.
  CONCATENATE wa1-field lv_suffix INTO lv_string.
  " 在这里进行操作
ENDLOOP.
  1. 使用 WHERE 条件:
* 不优化的代码
LOOP AT itab1 INTO wa1.
  LOOP AT itab2 INTO wa2 WHERE field = wa1-field.
    " 在这里进行操作
  ENDLOOP.
ENDLOOP.

* 优化的代码
SELECT * FROM table2 INTO TABLE itab2 WHERE field IN itab1-field.
LOOP AT itab1 INTO wa1.
  LOOP AT itab2 INTO wa2 WHERE field = wa1-field.
    " 在这里进行操作
  ENDLOOP.
ENDLOOP.
  1. 使用二进制搜索:
* 不优化的代码
LOOP AT itab1 INTO wa1.
  READ TABLE itab2 INTO wa2 WITH KEY field = wa1-field.
  IF sy-subrc = 0.
    " 在这里进行操作
  ENDIF.
ENDLOOP.

* 优化的代码
SORT itab2 BY field.
LOOP AT itab1 INTO wa1.
  READ TABLE itab2 INTO wa2 WITH KEY field = wa1-field BINARY SEARCH.
  IF sy-subrc = 0.
    " 在这里进行操作
  ENDIF.
ENDLOOP.
  1. 避免在循环内部进行递归调用:
* 不优化的代码
FORM recursive_call.
  LOOP AT itab1 INTO wa1.
    CALL FUNCTION 'recursive_call'.
  ENDLOOP.
ENDFORM.

* 优化的代码
FORM recursive_call.
  LOOP AT itab1 INTO wa1.
    " 在这里进行操作
  ENDLOOP.
ENDFORM.

LOOP AT itab1 INTO wa1.
  CALL FUNCTION 'recursive_call'.
ENDLOOP.

参考链接: