Incrementing a pass-by-reference value in a called routine on a hot path: 3 methods

by Finnian Reilly (modified: 2026 Aug 20)

Introduction

On a genuine hot path — a tokenizer inner loop, a byte classifier, a tight scan — even a single integer increment is not free, and its cost is not constant. When the integer you must update lives in a caller's variable, reached through a pass-by-reference argument, the way you read it and write it back decides how many machine instructions each update costs. This short piece benchmarks three ways to increment a 32-bit integer through such an argument in Eiffel: an inline C compound-assignment, a typed-pointer read-and-copy, and a boxed reference object. All three perform the same read-modify-write, so the comparison is like-for-like.

The three methods

All three live in a single routine, do_method, selected by an inspect and repeated 500,000 times per pass:

do_method (id: INTEGER; integer_ptr: TYPED_POINTER [INTEGER]; value: EL_INTEGER_32_REF) local i: INTEGER do from until i > Repetition_count loop inspect id when 1 then c_add_to_integer_32 (10, integer_ptr) when 2 then add_to_integer_32 (10, integer_ptr) when 3 then value.add (10) else end i := i + 1 end end

Method 1 is an inline C compound-assignment. The compiler emits the C directly into the caller, so the increment becomes a single read-modify-write against memory — no Eiffel routine-call overhead at all:

c_add_to_integer_32 (value: INTEGER; integer_ptr: TYPED_POINTER [INTEGER]) external "C inline use <eif_eiffel.h>" alias "*$integer_ptr += $value;" end

Method 2 goes through EL_TYPED_POINTER_ROUTINES_I. To increment it must first read the current value, then copy the sum back — and both the read and the write are general, byte-count–driven copies via memory_copy:

add_to_integer_32 (a_value: INTEGER; integer_ptr: TYPED_POINTER [INTEGER]) local value: INTEGER do value := read_integer_32 (integer_ptr) + a_value integer_ptr.memory_copy ($value, {PLATFORM}.Integer_32_bytes) end

The read side is itself a memory_copy, so one increment costs two general copies:

read_integer_32 (integer_ptr: TYPED_POINTER [INTEGER]): INTEGER do ($Result).memory_copy (integer_ptr, {PLATFORM}.Integer_32_bytes) end

Method 3 updates EL_INTEGER_32_REF, an INTEGER_32_REF subclass that adds an add routine. The read and write are direct field access on the boxed object — no byte-count copy:

add (value: INTEGER) do set_item (item + value) end

called on the hot path simply as value.add (10).

The harness

DEVELOPER_COMPARISON inherits EL_BENCHMARK_COMPARISON and hands each method to compare as an agent, timing how many passes complete inside a fixed window:

local value: INTEGER; l_value: EL_INTEGER_32_REF do create l_value compare ("perform benchmark", << ["method 1", agent do_method (1, $value, l_value)], ["method 2", agent do_method (2, $value, l_value)], ["method 3", agent do_method (3, $value, l_value)] >>)

Results

Passes completed in a 500 ms window, more is faster, method 1 as baseline, listed fastest first:

Method Mechanism Passes / 500 ms Relative
1 C inline compound-assignment (+=) 536 100% (baseline)
3 EL_INTEGER_32_REF.add (field access) 52 −90.3%
2 TYPED_POINTER read + copy-back (two memory_copy) 45 −91.6%

Note the ordering: the boxed reference object (method 3) beats the typed pointer (method 2), the reverse of what "closest to the metal wins" would predict.

Why the spread

Because all three now perform the same read-modify-write, the gaps are pure mechanism cost.

Method 1 is a single inlined compound-assignment. It compiles to a load–add–store — often one add-to-memory instruction — with no feature call and no general copy. It runs roughly ten times faster than either Eiffel alternative.

Method 2 is the slowest, and the reason is the read. Incrementing through memory_copy means one general, byte-count–driven copy to fetch the value (read_integer_32) and a second to store the sum (add_to_integer_32) — two runtime copies per iteration, each reached through a feature call. memory_copy is built for bulk transfers, not for a scalar read-modify-write in a tight loop, and doing it twice is what sinks it to −91.6%.

Method 3 reads and writes with item and set_item — direct field access through a single object reference. One indirection, a field load, an add, a field store. That is cheaper than two memory_copy calls, so the heaviest-looking abstraction actually edges ahead of the typed pointer for this operation.

The result overturns a tempting intuition: TYPED_POINTER.memory_copy is not "close to the metal." It is a general runtime routine, and needing it twice to increment a scalar costs more than plain field access on a boxed object.

Takeaway

  • If you own the memory and care about speed, an inline C compound-assignment wins outright — about ten times faster here.
  • Between the two pure-Eiffel options, a boxed reference with direct field access beats routing a read-modify-write through TYPED_POINTER.memory_copy twice.
  • Reserve memory_copy for bulk or one-shot transfers, not scalar updates in a hot loop.
  • Measure on your own toolchain and compilation mode: these numbers depend on the compiler and on whether the code is finalized or running in workbench mode.

Notes

  • Tested on my old Eiffel Studio 16.05/gcc compiler setup in finalized executable. I have not yet had time to migrate my benchmarking tools for v25.12 on my new machine.
  • Naturally the routines in EL_TYPED_POINTER_ROUTINES_I have been updated to use the C inline pointer store/read. The benchmarks were performed using the original version.