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

1

6

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.

Regazzi answered 13/2, 2023 at 12:17 Comment(9)
I never heard of any performance issue.Landwaiter
There is an overhead when exceptions are thrown but one cannot speak of an performance issue here. It is just the standard cost of using them. Also remeber that the exceptions are not designed for the standard flow of control e.g. for doing such things as GOTO statements. They are after all called exceptions which mean they should be used for extraordinary situations.Philipson
I don't understand why this question was closed as "opinion-based". There are a lot of objective things that can be said about the performance implications of exceptions - both those that happen and those that don't happen - and those statements can be proven or disproven by doing performance tests.Gingery
@Gingery You can always use the power of Stackoverflow's democracy and vote to reopen this question. :)Philipson
@Philipson I already did half an hour ago.Gingery
It can even be proven by looking at the bytecode that's available in the ABAP system (e.g. inside loop and outside loop)Postliminy
@Philipson This democracy is limited to 3,000 reputation though :)Postliminy
@Postliminy only voters that are experienced enough are allowed. :)Philipson
I should have made more clear that the question is about the TRY/CATCH construct itself, not its use when throwing/catching exceptions. So, asked differently, if a statement in a loop body can throw an exception, is there a difference between placing the TRY outside and inside the LOOP.Regazzi
S
6

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:

  • BRAX / BRAN: Branch always relative / Branch always. These are used to jump unconditionally to a specific location in the bytecode sequence.

  • EXCP: Exception Call. This is used to manage exception handling or to raise exceptions within the program.

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.

Sporty answered 9/8 at 13:23 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.