performanceloopstry-catchperformance-testingabap

Is there a performance cost when entering TRY/CATCH blocks?


The following is a question about the performance of the ABAP TRY/CATCH construct. In particular, it is not about throwing or catching exceptions.

Is there a performance cost when entering TRY/CATCH blocks? For example, if there is a loop and the TRY could be outside and inside the loop body, would there be a performance difference between the two options?

Again, it is not about throwing or catching exceptions, it is about entering and leaving TRY/CATCH blocks.


Solution

  • The performance difference between placing try-catch statements inside or outside a loop in ABAP is negligible, as the bytecode efficiently handles exceptions with minimal overhead. The impact is measured in microseconds, indicating that try-catch placement has little effect on runtime.


    To address the question and gain some insights, let’s approach the problem from two perspectives. First, devise and establish appropriate test cases, run and measure them, and then analyse what happens at a deeper level in the ABAP bytecode.

    Define the Test Cases

    Let's define four test cases to evaluate the performance impact of try-catch statements in different scenarios:

    1. <TRY [LOOP]>
      try/catch outside the loop
    2. [LOOP <TRY>]
      try/catch inside the loop
    3. <TRY [LOOP EXCP]>
      try/catch outside the loop with an exception thrown
    4. [LOOP <TRY EXCP>]
      try/catch inside the loop with an exception thrown

    Below, I will show the relevant parts of the code to illustrate the testing process.
    For looping, the DO keyword is used (in bytecode, it is translated into the WHILx opcode, like other looping statements).
    To measure the runtime the construct +REP x TIMES. ...code... +ENDREP RESULTS structure. is used. It repeats the code inside it x times and records time measurements in the structure of type REP_S_RESULTS, from which the component RTIME - gross time - is used for the calculations. The REP statement is directly translated into the bytecode REP opcode.

    ABAP coding for [LOOP <TRY>] and [LOOP <TRY EXCP>]:

      METHOD zif_measurement_test~run_test.
    
        DATA: l_d TYPE decfloat34, l_f TYPE f.
    
        +REP iv_measurement_repeats TIMES.
        DO iv_measurement_iterations TIMES.
          TRY.
              l_d = zcl_measurement_blackhole=>consume( ).
    
              mv_total_calculations = mv_total_calculations + 1.
    
              IF sy-index = iv_measurement_iterations.
    
                l_f = 1 / 1.   " test case with thrown exception has l_f = 1 / 0. in this line
    
              ELSE.
                l_f = 1 / 2.
              ENDIF.
    
            CATCH cx_root.
              mv_total_exceptions = mv_total_exceptions + 1.
          ENDTRY.
        ENDDO.
        +ENDREP RESULTS rs_results.
    
      ENDMETHOD.
    

    ABAP coding for <TRY [LOOP]> and <TRY [LOOP EXCP]>:

      METHOD zif_measurement_test~run_test.
    
        DATA: l_d TYPE decfloat34, l_f TYPE f.
    
        +REP iv_measurement_repeats TIMES.
        TRY.
            DO iv_measurement_iterations TIMES.
    
              l_d = zcl_measurement_blackhole=>consume( ).
    
              mv_total_calculations = mv_total_calculations + 1.
    
              IF sy-index = iv_measurement_iterations.
    
                l_f = 1 / 1.   " test case with thrown exception has l_f = 1 / 0. in this line
    
              ELSE.
                l_f = 1 / 2.
              ENDIF.
    
            ENDDO.
          CATCH cx_root.
            mv_total_exceptions = mv_total_exceptions + 1.
        ENDTRY.
        +ENDREP RESULTS rs_results.
    
      ENDMETHOD.
    

    The zcl_measurement_blackhole=>consume( ) method does not do anything special; just introduces some calculations to create a CPU load and obtain more scaled time values:

      METHOD consume.
        rv_double = compute_single( cv_pi ). + compute_single( cv_pi * 2 ).
      ENDMETHOD.
    
      METHOD compute_single.
        DATA: lv_index TYPE i VALUE 0.
    
        rv_double = iv_double.
    
        WHILE lv_index < 10.
          rv_double = rv_double * rv_double / cv_pi.
          lv_index = lv_index + 1.
        ENDWHILE.
      ENDMETHOD.
    

    The code was kept minimalistic and consistent across all test cases to execute the statements of interest and produce relevant time measures.

    Execution of Test Cases

    The four test cases were executed with 101, 501, 1001, and 5001 single runs, respectively. One single run (the code inside +REP ... +ENDREP) is 10 loop iterations repeated n times where n = 100 + single_runs_couter (i.e. increasing by 1 per single run). In the relevant test cases, one exception was thrown per 10 loop iterations.

    ...
        DO mv_measurement_total_tests TIMES.
          ls_test_stat = lo_t_trycatch_inside_loop->run_test( iv_measurement_repeats = mv_measurement_repeats iv_measurement_iterations = mv_measurement_iterations ).
          INSERT VALUE #( rtime = ls_test_stat-rtime / mv_measurement_repeats test_id = lo_t_trycatch_inside_loop->test_id )  INTO TABLE mt_results.
    
          ls_test_stat = lo_t_trycatch_outside_loop->run_test( iv_measurement_repeats = mv_measurement_repeats iv_measurement_iterations = mv_measurement_iterations ).
          INSERT VALUE #( rtime = ls_test_stat-rtime / mv_measurement_repeats test_id = lo_t_trycatch_outside_loop->test_id ) INTO TABLE mt_results.
    
          ls_test_stat = lo_t_trycatch_inside_loop_ex->run_test( iv_measurement_repeats = mv_measurement_repeats iv_measurement_iterations = mv_measurement_iterations ).
          INSERT VALUE #( rtime = ls_test_stat-rtime / mv_measurement_repeats test_id = lo_t_trycatch_inside_loop_ex->test_id ) INTO TABLE mt_results.
    
          ls_test_stat = lo_t_trycatch_outside_loop_ex->run_test( iv_measurement_repeats = mv_measurement_repeats iv_measurement_iterations = mv_measurement_iterations ).
          INSERT VALUE #( rtime = ls_test_stat-rtime / mv_measurement_repeats test_id = lo_t_trycatch_outside_loop_ex->test_id ) INTO TABLE mt_results.
    
          mv_measurement_repeats = mv_measurement_repeats + 1.
        ENDDO.
    ...
    

    The gross runtime from all tests was recorded in the mt_results table and is summarised in the table below. Here, the median, average, and standard deviation are calculated for 10 loop iterations in microseconds for each test case. Additionally, the totals of single runs and loop executions per test are shown:

    Value, ms Runs Loops <TRY [LOOP]> [LOOP <TRY>] <TRY [LOOP EXCP]> [LOOP <TRY EXCP>]
    Median
    10 loops
    101
    501
    1001
    5001
    151.5K
    1.75M
    6.01M
    130.03M
    55.31
    55.19
    55.50
    56.50
    55.53
    55.49
    55.97
    56.75
    57.65
    57.91
    57.91
    59.04
    57.78
    57.95
    58.05
    59.25
    Average
    10 loops
    101
    501
    1001
    5001
    151.5K
    1.75M
    6.01M
    130.03M
    55.34
    56.25
    56.61
    57.73
    55.63
    56.70
    56.76
    57.98
    57.61
    59.16
    58.80
    60.41
    57.92
    59.18
    59.15
    60.62
    σ
    10 loops
    101
    501
    1001
    5001
    151.5K
    1.75M
    6.01M
    130.03M
    0.68
    3.92
    6.29
    3.86
    0.44
    4.95
    3.44
    3.75
    0.30
    5.73
    3.53
    4.42
    0.91
    5.15
    5.06
    4.40

    As we can see, there is no significant performance difference based on the position of the try-catch statements. The total time difference across all tests is only on the order of microseconds.

    Bytecode details

    To look deeper, let’s delve into the bytecode level. The source code is first compiled into bytecode. It is the intermediate low-level representation of the program consisting of a set of instructions (opcodes) interpreted by the SAP kernel. These are further translated into the binary representation for the target platform to execute ABAP programs.

    Several opcodes are relevant for understanding what happens in our test cases:

    While I won’t delve into all opcodes and their arguments due to the closed nature of bytecode specifications, we can deduce enough to explain the measurements above.

    Below is the bytecode compiled for the test cases, with extraneous lines / details removed for clarity and comments added:

    INSIDE LOOP

    27 METH 14 0000 start of method
    42 REP 00 0000 +REP expression
    46 WHIL 00 0002 instantiating the loop (DO.)
    50 whli 01 0003 checking the loop condition
    54 BRAN 05 0027 when loop condition is false jump to 93

    55 EXCP 09 0000 setup of exception handling machinery (TRY.)
    56 BRAX 00 001B jump point if no exception was thrown, i.e. skip CATCH block

    57 clcm 10 0001 call blackhole method
    64 ccsi 4B C006 increase mv_total_calculations counter
    68 cmpb 04 00F2 checking if it is the last loop iteration
    72 ccqf CE 0000 first division
    76 BRAX 00 0005 else
    77 ccqf CE 0000 second divison

    81 EXCP 08 0000 exception handler
    82 BRAX 00 0009
    83 EXCP 00 0003 exception handler
    84 BRAX 00 0007
    85 EXCP 07 0000 exception handler (CATCH cx_root.)
    86 ccsi 4B C007 increase mv_total_exceptions counter
    90 BRAX 00 0001
    91 EXCP 0B 0000 end of exception handling machinery (ENDTRY.)

    92 BRAX 00 FFD6 jump to the loop checking condition
    93 WHIL 00 0004 end of looping construct (ENDDO.)
    97 EREP 00 C002 +ENDREP expression
    98 METH 01 0000 end of method

    OUTSIDE LOOP

    32 METH 14 0000 start of method
    42 REP 00 0000 +REP expression

    46 EXCP 09 0000 setup of exception handling machinery (TRY.)
    47 BRAX 00 0029 jump point if no exception was thrown, i.e. skip CATCH block

    48 WHIL 00 0002 instantiating the loop (DO.)
    52 whli 01 0003 checking the loop condition
    56 BRAN 05 001A when loop condition is false jump to 82
    57 clcm 10 0001 call blackhole method
    64 ccsi 4B C006 increase mv_total_calculations counter
    68 cmpb 04 00F2 checking if it is the last loop iteration
    72 ccqf CE 0000 perform first division
    76 BRAX 00 0005 else
    77 ccqf CE 0000 perform second division
    81 BRAX 00 FFE3 jump to 52 (loop checking condition)
    82 WHIL 00 0004 end of looping (ENDDO.)

    86 EXCP 08 0000 exception handler
    87 BRAX 00 0009
    88 EXCP 00 0003 exception handling
    89 BRAX 00 0007
    90 EXCP 07 0000 exception handler (CATCH cx_root.)
    91 ccsi 4B C007 increase mv_total_exceptions counter
    95 BRAX 00 0001
    96 EXCP 0B 0000 end of exception handling machinery (ENDTRY.)

    97 EREP 00 C002 +ENDREP expression
    98 METH 01 0000 end of method

    In the “Inside Loop” scenario, the EXCP opcode is executed in each loop iteration to set up exception handling. However, because the program is already loaded and allocated in memory, the target addresses of handlers are pre-determined and do not need recalculation each time. Due to optimisations, the exception handlers resolution table might be created once for the program and referenced afterward.

    When no exception is thrown, the catch blocks are bypassed. However, if an exception is thrown, the appropriate handler is resolved and invoked, resulting in some overhead.

    Link to the code on github.