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.
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:
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.