Sign in Join

Live chat for Free Pascal, Lazarus, Delphi and every Pascal programmer.

Mailing lists / fpc-devel / 2019-03

Re: Successful implementation of inlinesupportforpureassembler routines on x86

4 messages · 2 people · –

Reply by email Subscribe to this list Your subscription

Read-only archive. Use j and k to move between messages.

J. Gareth Moreton

Just wrote some Pascal functions for ClampInt to test it:

 function ClampInt1(X: LongInt): LongInt; inline;
 begin
   if (X < 0) then
     Result := 0
   else
     Result := X;
 end;
 function ClampInt2(X: LongInt): LongInt; inline;
 begin
   Result := 0;
   if (X > 0) then Result := X;
 end;
 The first one produces relatively convoluted assembly language when
calling "I := ClampInt1(I);"

0000000100001634 89f8                     mov    %edi,%eax   To use the integer clamp function as an example (if x < 0 then x := 0):

{ Microsoft x64 calling convention... X is in ECX } function ClampInt(X: LongInt): LongInt;
 assembler; nostackframe; inline;
Show quoted text 5 lines
asm   XOR EAX, EAX   TEST ECX, ECX   CMOVG EAX, ECX end;
 try code:
 y:=0;
 if x < 0 then x:=y;

Reply by email

J. Gareth Moreton

I suppose what I'm trying to say is that I don't trust the compiler to actually use a register in the intermediate stages, especially in larger procedures.  To use AES-NI as an example, will it write back the partially-encrypted block to the stack after every round, or only after all the rounds have been completed? It would be perfectly logical to use the same variable to store the intermediate stage and the final stage if using a chain of intrinsics.  e.g:

 ciphertext := _mm_pxor(plaintext, roundkey[0]);
 for x := 1 to 9 do
   ciphertext := _mm_aesenc(ciphertext, roundkey[x]);
 ciphertext := _mm_aesenclast(ciphertext, roundkey[10]);

Of course, it's probably better to manually expand the for-loop here no matter if intrinsics or inline assembly is used, but I wouldn't want ciphertext to be written to the stack or some other backing store until at least the last round.  Unless another variable is used, I can see the compiler writing ciphertext to the stack after every intermedate step.  Granted a lot of it is down to the programmer. e.g:

 intermediate := _mm_pxor(plaintext, roundkey[0]);
 intermediate := _mm_aesenc(intermediate, roundkey[1]);
 intermediate := _mm_aesenc(intermediate, roundkey[2]);
 intermediate := _mm_aesenc(intermediate, roundkey[3]);
 intermediate := _mm_aesenc(intermediate, roundkey[4]);
 intermediate := _mm_aesenc(intermediate, roundkey[5]);
 intermediate := _mm_aesenc(intermediate, roundkey[6]);
 intermediate := _mm_aesenc(intermediate, roundkey[7]);
 intermediate := _mm_aesenc(intermediate, roundkey[8]);
 intermediate := _mm_aesenc(intermediate, roundkey[9]);
 ciphertext := _mm_aesenclast(intermediate, roundkey[10]);

Or a messier one but which is more likely to guarantee that the intermediate stages remain in registers:

ciphertext := _mm_aesenclast(
  _mm_aesenc(
    _mm_aesenc(
      _mm_aesenc(
        _mm_aesenc(
          _mm_aesenc(
            _mm_aesenc(
              _mm_aesenc(
                _mm_aesenc(
                  _mm_aesenc(
                    _mm_pxor(plaintext, roundkey[0]),                   roundkey[1]),
                roundkey[2]),
              roundkey[3]),
            roundkey[4]),
          roundkey[5]),
        roundkey[6]),
      roundkey[7]),
    roundkey[8]),
  roundkey[9]),
roundkey[10]);

This is where I feel that inline assembler is a little cleaner and guarantees a degree of certainty with efficiency.

Gareth aka. Kit

On Mon 18/03/19 07:00 , "Sven Barth" sent:
J. Gareth Moreton schrieb am So., 17. März 2019, 23:27: I think one of the main issues with intrinsics is that you don't have much control over where results are stored.  Unless you're chaining a load of intrinsics together in a mess of function calls in actual parameters, the result is going to have to be stored in a local variable, which even on good days will end up being stored on the stack, and problems can occur if the stack isn't aligned and you're using SSE or AVX instructions.  After that, when you call the next intrinsic that uses the result, it will have to recall that data from memory. 

And I believe that this is the advantage of intrinsics, because here the compiler *can* decide to use a different register. Especially if the compiler supports instruction scheduling and such. At work I've worked with AES-NI and I definitively preferred to work with the intrinsics and didn't have to care about what registers to use, because the compiler and optimizer took care of that.  That is something that Pascal should stand for: ease of use. Assembler is not easy to use. 
Regards, Sven 

Reply by email

J. Gareth Moreton

I think that is the fundamental difference between intrinsics and reusable, inlinable assembly language... you would use intrinsics mostly to use one or two special instructions in a convenient, concise way, while assembly language is a "give me all the power!" request.  Admittedly though my patch does allow the peephole optimizer to touch the assembly language in the inlined procedure, mostly to help optimize parameter and result passing.
Using assembly language always carries risk - that's pretty much what a programmer signs up for if they choose to use it.  The ability to inline particular assembler routines is a means to give as much power and trust as possible to the programmer.  If the program breaks because of what they've coded, it's on them to fix it.  Besides, when it comes to debugging, I've found that I comment out all of my "inline" directives because of how even regular inlined procedures cause problems with the code stepper.  The only thing I want to minimise as much as possible when it comes to developing the compiler is the chance of code behaving differently depending on whether it's inlined or not.
I'll admit that I'm stubborn and sceptical with certain things, not least because I know the Free Pascal Compiler has limitations that are not easily addressed.  For a simple example, if you try to do "x div 10" and "x mod 10" close to each other, the compiler will write code to perform each calculation separately rather than being more efficient and performing the division once (using a multiplication trick for speed) and calculating the remainder via "R := x - (Q * 10)", where Q is the result of "x div 10".  It's even worse if the divisor is a variable.  Something like the Microsoft Visual C++ compiler will spot this and combine the calculations into a single DIV instruction, using the results in EAX and EDX as appropriate, while Free Pascal will perform the calculations separately.  It's not easy to peephole-optimize either because the second DIV instruction may use a different register for the divisor.

Gareth aka. Kit

P.S. This is where my work on a "deep optimizer" comes into play, as it attempts to perform data flow analysis on the used registers, although only works on MOV instructions currently.  It works fairly well currently, but would work better if the step where virtual registers are converted into real ones is performed last (or right before the post-peephole optimization stage), since this step also allocates stack space that might not be needed if optimization is able to remove the use of a register completely and hence free it up for something that would otherwise spend its time on the stack.

On Mon 18/03/19 11:05 , Marco van de Voort
sent:

Op 3/18/2019 om 8:00 AM schreef Sven Barth via fpc-devel: J. Gareth Moreton schrieb am So., 17. März 2019, 23:27:

And I believe that this is the advantage of intrinsics, because here the compiler *can* decide to use a different register. Especially if the compiler supports instruction scheduling and such.  At work I've worked with AES-NI and I definitively preferred to work with the intrinsics and didn't have to care about what registers to use, because the compiler and optimizer took care of that.

(well, better double check output, it is not always ideal)

I've seen nice examples in simd lib
(http://ermig1979.github.io/Simd/index.html [2]), where they use generics to bundle intrinsics into blocks, and then reuse them multiple times, e.g. 3 times for the first, bulk and last line of an image. That is something that Pascal should stand for: ease of use. Assembler is not easy to use. 

If something is generic enough to be an intrinsic, it should be an intrinsic and as secured as much as possible.

Inlinable assembler however is something to get some of that defining power also in the user's hand. It doesn't really matter that there are border cases, as long as they can be described, since assembler is intrinsically unportable anyway. But having something like that is quite important I think. Though examples come more from my embedded, and less from my PC work (even though I use AVX2 there. Intrinsics would be better for many cases)

Reply by email

Jonas Maebe
to J. Gareth Moreton
J. Gareth Moreton wrote 7 lines
I think that is the fundamental difference between intrinsics and reusable, inlinable assembly language... you would use intrinsics mostly to use one or two special instructions in a convenient, concise way, while assembly language is a "give me all the power!" request. Admittedly though my patch does allow the peephole optimizer to touch the assembly language in the inlined procedure, mostly to help optimize parameter and result passing.

No optimizer must ever be allowed to interact with inline assembly (apart from parametrised operands related to constraints). Inline assembly is completely the responsibility of the programmer, and must always appear literally in the output. It is impossible to analyse all possible tricks a programmer may use and safely handle those in an optimizer.

Similarly, replacing return instructions with jumps is something you cannot do. You can do that with Pascal-level "exit" statements, but not with assembler-level return instructions. As you wrote yourself: "If the program breaks because of what they've coded, it's on them to fix it."

That is again why I argue for never allowing the inlining of pure assembler routines, and only for inlining Pascal routines that happen to also contain inline assembly. In that case, you will (almost?) never encounter the case where a routine works if it's not inlined, but breaks once it's inlined, without needing any warnings or target-specific analyses.

Jonas

Reply by email