Atomic increment benchmarks

I wrote a small benchmark program to compare three methods of implementing atomic increment in Ada:

  • Classic Ada protected object
  • System.Atomic_Operations.Integer_Arithmetic (since Ada 2022)
  • GCC built-in

Here is the code:

pragma Ada_2022;

with Ada.Calendar;   use Ada.Calendar;
with Ada.Text_IO;    use Ada.Text_IO;
with Interfaces.C;   use Interfaces.C;

with System.Atomic_Operations.Integer_Arithmetic;

procedure Lock_Free_Benchmark is

   function Add
            (  Ptr      : access Integer;
               Val      : Integer;
               Memorder : int := 0
            )  return Integer;
   pragma Import (Intrinsic, Add, "__atomic_add_fetch_4");

   protected Protected_Integer is
      procedure Increment;
      function Get return Integer;
   private
      Value : Integer := 0;
   end Protected_Integer;

   protected body Protected_Integer is
      procedure Increment is
      begin
         Value := Value + 1;
      end Increment;
      function Get return Integer is
      begin
         return Value;
      end Get;
   end Protected_Integer;

   type Atomic_Integer is new Integer with Atomic;
   package Atomic_Integers is
      new System.Atomic_Operations.Integer_Arithmetic (Atomic_Integer);
   use Atomic_Integers;

   Start      : Time;
   T1, T2, T3 : Duration;
   X          : aliased Atomic_Integer := 0;
   Y, Z       : aliased Integer := 0;
   Times      : constant := 100_000;

   procedure Report (T : Duration; Text : String) is
   begin
      Put (Text);
      Put (Duration'Image (T));
      Put (Integer'Image (Integer ((T * 1000_000_000) / Times)));
      Put ("ns ");
      New_Line;
   end Report;

begin
   Start := Clock;
   for I in 1..Times loop
      Protected_Integer.Increment;
   end loop;
   T1 := Clock - Start;

   Start := Clock;
   for I in 1..Times loop
      Atomic_Add (X, 1);
   end loop;
   T2 := Clock - Start;

   Start := Clock;
   for I in 1..Times loop
      Z := Add (Y'Access, 1);
   end loop;
   T3 := Clock - Start;

   Put_Line ("Method             Total       Time");
   Report (T1, "protected object: ");
   Report (T2, "atomic integer:   ");
   Report (T3, "gcc built-in:     ");

end Lock_Free_Benchmark;

As expected the second two are almost same. Protected object is 2/3 times slower, which is quite good in my view.

Here are measures.

Debian:

Method             Total       Time
protected object:  0.000938000 9ns
atomic integer:    0.000536000 5ns
gcc built-in:      0.000382000 4ns

Windows:

Method             Total       Time
protected object:  0.001584700 16ns 
atomic integer:    0.000534000 5ns 
gcc built-in:      0.000384800 4ns

Interestingly, Linux is faster, maybe because it uses a simpler locks than Windows. Remember discussion on external protected action which block under Linux but does not under Windows? It seems that Windows is slower for that same reason.

6 Likes

Thanks for sharing, out of curiosity what compiler flags did you use for the executables? (assume the same in both machines?)

-O2, however in this case it seems has no visible effect.

1 Like

Did you check the generated assembly code? GNAT will use atomic operations for protected objects when possible.

Good point! It indeed does this when Lock_Free aspect is used. If I change the line 18 to:

protected Protected_Integer with Lock_Free is

Then the results look like this under Windows:

Method             Total       Time
protected object:  0.000658100 7ns 
atomic integer:    0.000531200 5ns 
gcc built-in:      0.000382000 4ns

Of course, in real applications a lock-free implementation would be almost never possible, especially on lousy RISC hardware. But it is great to have this option.