Use GCC builtin intrinsic that requires immediate value as argument

Hi, firstly I wanted to thank those who started and participate on this website and forum as I was debating whether to ask this on Stackoverflow or here, but at least from briefly checking, this forum possibly seems to be a little more active than Ada-related topics on stack overflow.

This may be somewhat of rarer topic, and it likely possible that I may have to ask someone more closely related to GNAT and or GCC directly to remedy this problem, but I thought it was worth trying here first.

As some may already know, GNAT allows binding to or importing GCC ‘builtin’ functions that are lower level related features provided directly by the compiler itself (i.e. not an external library that is linked to). These builtin functions also include ‘intrinsic’ functions that provide language-level mappings to specific assembly instructions like SIMD ones.

Specifically, I have been trying to use x86 intrinsic __builtin_ia32_psrldqi128 which shifts the contents of a 128 bit XMM register to the right by the specified number of bytes. The problem is that the second argument, the number of bytes to shift by, must be an immediate value that is encoded within the instruction directly (i.e. not in a register), which when used in Ada looks like

with ada.text_io; use ada.text_io;                                                                                                                                                       
                                                                                                                                                                                         
with gnat.sse;                                                                                                                                                                         > 
with gnat.sse.vector_types;                                                                                                                                                              
                                                                                                                                                                                         
procedure test_program is                                                                                                                                                                
                                                                                                                                                                                         
   package sse_vt renames gnat.sse.vector_types;                                                                                                                                         
   package sse renames gnat.sse;                                                                                                                                                         
                                                                                                                                                                                         
   function ia32_psrldq (V : sse_vt.m128i; Imm : integer) return sse_vt.m128i                                                                                                            
     with import,                                                                                                                                                                        
          inline_always,                                                                                                                                                                 
          external_name => "__builtin_ia32_psrldqi128",                                                                                                                                  
          convention => intrinsic;                                                                                                                                                       
                                                                                                                                                                                         
   type vf32_view is array (positive range 1 .. 4) of sse.float32 with alignment => sse.vector_align;                                                                                    
                                                                                                                                                                                         
   v1 : sse_vt.m128i;                                                                                                                                                                    
   v1_view : vf32_view := (1.0, 1.0, 1.0, 1.0) with address => v1'address;                                                                                                               
                                                                                                                                                                                         
   v2 : sse_vt.m128i;                                                                                                                                                                    
   v2_view : vf32_view with address => v2'address;                                                                                                                                       
                                                                                                                                                                                         
begin                                                                                                                                                                                    
   v2:= ia32_psrldq (v1, 1);
   put_line (v2_view (1)'image & v2_view (2)'image & v2_view (3)'image & v2_view (4)'image);                                                                                                                                                             
end test_program;

C and C++ are able to use these as expected as long as the argument which is an immediate value is a constant, static expression known at compile time (i.e. not a variable), which would look like it does in the Ada example above at v2:= ia32_psrldq (v1, 1) where 1 is the immediate value, but when used in Ada it seems no matter how the argument is specified GCC interprets it as a non-immediate value (I say GCC specifically because the message appears to be emitted by a backend stage instead of GNAT itself).

The specific message it gives is (using GNAT 14.1.0 on macOS)

test_program.adb: In function 'test_program':
test_program.adb:26:9: error: the last argument must be an 8-bit immediate
gnatmake: "test_program.adb" compilation error

From I have found so far the best alternative (and likely only) option for now is to use inline assembly where constraints can be given that specifically convey to the compiler how a particular argument value should be interpreted. I have found other discussions about using other builtin intrinsic functions in Ada but none that specifically import ones that require an immediate value as argument (though it is possible I missed something).

I assume this is likely an oversight or bug within the Ada frontend of GCC due to how rarely intrinsics that require this have been used within Ada, but maybe there is something I missed, or if not, that maybe this can be fixed or added to GNAT at some point.

Thank you for reading and for any advice

This test program is build successfully by GCC 15 and GCC 16 on Linux.

It might be a bug in GCC 14 compiler.

Thank you for your reply and for trying the test @godunko, though I think it might need to be run a second time to be certain. I had already experienced this in another situation but didnt try checking the test program; that is if you had any level of optimization enabled it seems that since the variable the return value is written to is never used, GCC optimizes out the entire call to the intrinsic function (the instruction isn’t present when checking the temporary assembly file), I added a call to put_line to print the result that should hopefully prevent that from happening (it seems to on my system at least when using -O1).

I also tried using GCC 15.3.0 and the same situation occurs, and although unlikely I will try on Linux as well just incase.

Edit: The same problem happened with GCC 12.2.0 on Linux as well

You have

and the error message is

Perhaps making Imm an 8-bit type would help?

Sorry, thank you for pointing that out. That was something I tried but I had already tested so many different things I guess I forgot to take note of it.

The signature for that intrinsic function in GCCs documentation actually just lists int for the second parameter and when trying to use an 8 bit data type (Interfaces.Integer_8) it shows an additional warning (not an error) but then still proceeds to show the same error as in other cases afterwards

test_program.adb:13:13: warning: intrinsic binding type mismatch on parameter 2 [enabled by default]
test_program.adb:13:13: warning: profile of "ia32_psrldq" doesn't match the builtin it binds [enabled by default]
test_program.adb: In function 'test_program':
test_program.adb:28:9: error: the last argument must be an 8-bit immediate

You may need a machine code insertion.

Looking at gcc source the immediate is always a multiple of 8. If you pass 8, 16, 24, and so on the program will compile and emit psrldq xmm0, 1, psrldq xmm0, 2, psrldq xmm0, 3 respectively. The error message is misleading.

The documented intel intrinsics for this instruction are wrappers around the builtin that do this transformation for you:

  1. _mm_bsrli_si128
  2. _mm_srli_si128

Oh, well then I think it all makes sense now… thank you very much for looking into it that far.

Usually I will go and try to look at the source code of something I am trying to use if something appears to be going wrong, but I guess since all the documentation that I read for the underlying instruction always mentioned the shift was done in units of bytes (and not bits) at least at the assembly level, it did not cross my mind. I also shouldn’t have just assumed that this was a GNAT bug (although my reasoning was probably influenced some by recently encountering other things that I think are actually bugs).

Indeed… the intrinsic wrapper multiplies it by 8 to convert from bits to bytes (at least I assume that is what its doing for __N * 8?). Also thank you for explaining the intrinsic part and linking to the GCC source file as I understand better now how the Intel intrinsic and builtin functions relate to and interact with each other.

Though I wonder why the GCC builtin expects the value to be in bits when the actual instruction it maps to uses bytes. Regardless yes I guess the error message is something that should be improved (I imagine it is being emitted generically at a lower level), or at least the GCC documentation for the intrinsics mentioning that the value is specified in bits, but I guess most people using C/C++ are just accessing it via emmintrin.h and not the builtin functions themselves.