Sign in Join

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

Mailing lists / fpc-devel / 2019-03

Re: Successful implementation of inline support forpureassembler routines on x86

10 messages · 4 people · –

Reply by email Subscribe to this list Your subscription

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

J. Gareth Moreton

Thank you so much Ben!  I'm glad I'm not the only person who gets a kick out of assembly language programming!  There's still a place for it even in today's world.
I myself have a passion for games development, and it was partly my driving force behind implementing "vectorcall" for FPC, something which only existed in Microsoft Visual C++ before.  I figured inlined assembler functions for certain mathematical functions would be hugely beneficial for gaming.  I've never got to grips with intrinsics because I'm not sure how much control I get - I prefer dropping into pure assembler.

If you're curious, have a look at the vectorcall tests in the "tests/test/cg" subdirectory of the FPC source (there should be about 4 or so, named tp the theme of "tvectorcall*.pp") - you'll find an implementation of TM128 there as well (also aligned to a 16-byte boundary as required by vectorcall) that, if I recall, has a useful mapping to X, Y, Z and W fields.
It's been a little hard trying to convince Florian of the benefits of inlined assembler routines because of the different assembler platforms that need supporting, which is why I had to design it very carefully in order to a) make sure other platforms don't break, and b) try to be as safe as possible (so no writing to the stack or non-volatile registers, for example, but who knows, maybe the restrictions can be loosened over time when proven safe).

Gareth aka. Kit

On Sun 17/03/19 01:54 , "Ben Grasset" sent:
Inlining of pure assembler functions would actually be immediately, specifically useful to me! I've been having a go at improving FPC scores on "BenchmarksGames", and was so far successful with Binary Trees simple by throwing a really good threading library at it, however, there are some benchmarks that simply can't be fixed without either proper intrinsics or user-specifiable inlinable ASM methods. I have a working re-implmentation of NBody (that is just a direct rewrite of the Rust implementation) where I've implemented __m128 and __m128d as records with static nostackframe assembler "class functions", however it's just not fast enough to be competitive due to the inability to inline any of the assembler methods. Something along these lines is absolutely sorely lacking in FPC currently, don't let anyone tell you otherwise.
On Sat, Mar 16, 2019 at 6:03 PM J. Gareth Moreton wrote: Of course, my worry now is that we've submitted so many patches and issues that we'll just be building an ever-growing back-log that may never be cleared.  It also depends on what Florian's own vision for the future of Free Pascal is, I think.
Gareth aka. Kit

On Sat 16/03/19 17:05 , "J. Gareth Moreton"
sent:
Normally Florian or another administrator will say if the patch has been applied and mark the ticket as "resolved" if they're happy.  Once they are added I will have a play around.

Admittedly one thing I'm also waiting on is the node XML dump feature ( https://bugs.freepascal.org/view.php?id=35017 [1] ), since that will allow me to see how procedures are constructed at the intermediate level, plus I sort of need it in order to work out what's going on with https://bugs.freepascal.org/view.php?id=32913 [2] , something I've vowed to fix but has been sitting dormant for ages now - I'll have to apologise to David Hawk (the reporter) afterwards.

Gareth aka. Kit

On Sat 16/03/19 16:07 , Ryan Joseph sent:

On Mar 15, 2019, at 9:37 AM, Sven Barth via fpc-devel wrote: That could maybe be managed once the support for constants as parameter

for generics is added (note: I don't know right now how SHUFPS works, so take the following as pseudo code):

I fixed the patch for constants in generics and uploaded again (https://bugs.freepascal.org/view.php?id=35140). [3] Not sure if they added it yet or they were waiting for my to fix things. Let me know if that patch is in the correct format so I can fix the other one for multi-helpers.

Regards,
Ryan Joseph

Reply by email

J. Gareth Moreton

Hi Ryan,
I haven't forgotten about "pure" functions - that's still definitely an area of research for me.  I got myself a little bit lost because I'm still trying to work out a solid design plan (for major changes to FPC, I now write a full specification, which are the PDF files I've attached to the bug issues for my x86_64 overhaul and the inline assembly).  Additionally, I realised I would need a better means of analysing the intermediate node tree to properly study how a procedure goes together and to get a good idea as to what can be pure and what can't, hence the XML dump feature. I hate to say it, but I've kind of blocked myself with a lot of things... I can't make any more peephole optimizer improvements until the x86_64 overhaul is merged or outright rejected, and a bug and the "pure" feature are blocked until the XML debug node dump feature is approved. Gareth aka. Kit

On Sun 17/03/19 02:04 , "Ryan Joseph" sent:

On Mar 16, 2019, at 9:54 PM, Ben Grasset wrote: Inlining of pure assembler functions would actually be immediately,

specifically useful to me! I've been having a go at improving FPC scores on "BenchmarksGames", and was so far successful with Binary Trees simple by throwing a really good threading library at it, however, there are some benchmarks that simply can't be fixed without either proper intrinsics or user-specifiable inlinable ASM methods. I have a working re-implmentation of NBody (that is just a direct rewrite of the Rust implementation) where I've implemented __m128 and __m128d as records with static nostackframe assembler "class functions", however it's just not fast enough to be competitive due to the inability to inline any of the assembler methods.

Something along these lines is absolutely sorely lacking in FPC

currently, don't let anyone tell you otherwise.

Sounds exciting progress for FPC. Btw what happened to the development of “pure” function modifier that would make it possible to use functions in compile time expressions? I was pretty excited about what could be done with that also.

Regards,
Ryan Joseph

Reply by email

Ryan Joseph
to J. Gareth Moreton
On Mar 16, 2019, at 10:25 PM, J. Gareth Moreton wrote: I hate to say it, but I've kind of blocked myself with a lot of things... I can't make any more peephole optimizer improvements until the x86_64 overhaul is merged or outright rejected, and a bug and the "pure" feature are blocked until the XML debug node dump feature is approved.

That’s too bad. Knowing if a function is “pure” or not seems deceptively simple but what were the big challenges btw?

Regards,
Ryan Joseph

Reply by email

J. Gareth Moreton

True, the assembler reader needs to be programmed to recognise new instructions - I myself implemented BMI a few months ago.  That's an issue with all assemblers... they need to be updated when a new instruction set comes out.
I do have a slight concern when it comes to intrinsic support, because support will be needed to be added for all platforms, (virtually) all instructions and tested to see if they expand into the expected instruction, along with determining what happens when you string several intrinsics together (i.e. passing the result of one such intrinsic into the actual parameter of another).  I want to say that it will require just as much, if not more, maintenance than permitting inlined assembler routines under limited circumstances, including adding intrinsics for every individual platform's assembly language and showing that it works.

P.S. I'll make fixes so it bans the use of DB etc in order to emit raw machine code, as well as catch naughty trickery in order to write to the stack via another register (it's easy enough to detect if a register has a dependency on ESP or EBP) - e.g. "mov %rsp, %rax; mov $0, (%rax)" - and trust me... don't try getting around it by setting a register to zero and then ADDING the stack pointer to it!

Gareth aka. Kit

P.S. It goes to show though... it's always possible to break assembly language if you try hard enough, whether or not it's inlined.  The intention behind the restrictions is to prevent as many accidental causes of it as possible.

On Sun 17/03/19 20:47 , Martok sent:

Florian Klämpfl wrote
How is it better than intrinsics support (similiar to gcc/icc etc.)?

It *exists*?

Remember how long it took to get PopCnt support? How about the rest of the BMI?
TBM? AES-NI? Newer AVX?

Implementation of these would be a lot easier if they could be written in code,
even with native-pascal implementations as drop-ins for platforms that don't
support them (or use other mnemonics). Those would just be normal declarations
(in the RTL or in a package), and not require modifying the compiler for all
platforms.
It would also be easier to maintain, as there wouldn't be the need for new

inlinennodes or entirely different node types.

Regards, Martok

Reply by email

J. Gareth Moreton

My take on the whole "inlined assembler routines" vs. "intrinsics"... why not both?  They both fill a different niche, and as I've stated, my feature doesn't replace intrinstics because you can't directly encode SHUFPS with them, for example.
Me personally, if I'm going to be playing around with raw CPU instructions, I rather drop directly into pure assembly language than use intrinsics, but I can see the benefit of using an intrinsic for a single instruction like, say, PREFETCHNTA or MFENCE (which admittedly have wrapper functions for them).
For __mm128, I declared a type that resembled it in the "tests/test/cg/tvectorcall*.pp" tests in order to test the vectorcall calling convention.  The main sticking point is that it must be aligned to a 16-byte boundary, whereas the System V ABI doesn't have that restriction, I don't think... it just uses the unaligned move operations instead.

Gareth aka. Kit

On Sun 17/03/19 21:14 , Jeppe Johansen sent:

Florian Klämpfl wrote 13 lines
Am 17.03.19 um 21:47 schrieb Martok: > Am 17.03.2019 um 18:57 schrieb Florian Klämpfl: >> How is it better than intrinsics support (similiar to gcc/icc etc.)? > It *exists*? > > Remember how long it took to get PopCnt support? PopCnt is not really an intrinsic as it has a fallback counter part and works on all platforms. Intrinsic means that it is really mapped directly to the CPU instruction without any fallbacks. As the branch of Jeppe shows, it is pretty easy, just requires some continuous work.

As far as I recall there were some issues in argument passing and handling the new __mm128 type that were a little annoying. But it might be fairly easy to bring back up to speed again.

> How about the rest of the BMI? > TBM? AES-NI? Newer AVX? See above.

Agreed if that's the route that's taken. It's my feeling that the newer extensions took a much more regular/orthogonal route than the old weird MMX and SSE forms.

Reply by email

Florian Klämpfl
to J. Gareth Moreton
J. Gareth Moreton wrote
My take on the whole "inlined assembler routines" vs. "intrinsics"... why not both?

Maintenance effort.

They both fill a different niche,

What niche do inline assembler routines fill, intrinsics dont't ? This is imo the central question. What can be done by inline assembler routines, intrinsics cannot?

Reply by email

J. Gareth Moreton

Speaking of POPCNT, there's an interesting webpage here regarding assembly v. intrinsics in C++: https://danluu.com/assembly-intrinsics/

One point that's been raised is that on older Intel processors, POPCNT has a bug in that it has a false dependency on the destination register, so it will cause a pipeline stall.  Though it's a bit of an exceptional case, it shows where inline assembly has an advantage over intrinsics because you have finer control over how temporary values are stored and also the order in which instructions appear in order to handle issues like that with POPCNT.

I do agree that you should program in Pascal whenever you can for readability and portability, and I will certainly continue to improve the x86 peephole optimizer in that regard, but there will always be cases where assembly language and even inlined assembly language will have a place.  I say give people the choice.

True, implementing it on other platforms will take time and will need a programmer with intricate knowledge of that platform's instruction set (I only have access to x86 machines currently), but it's not a super-critical task and doesn't actually break the compiler by not being present.  Still, when the day comes that I get an ARM-powered Raspberry Pi, I'll happily start researching its assembly language to improve FPC on there as well, especially if people start demanding it.  I'm under the impression that, generally, i386 and x86_64 are two of the most popular platforms when it comes to development of FPC projects, so it makes sense to target those platforms first.

Gareth aka. Kit

Martok wrote 5 lines
Am 17.03.2019 um 18:57 schrieb Florian Klämpfl: > How is it better than intrinsics support (similiar to gcc/icc etc.)? It *exists*? Remember how long it took to get PopCnt support?

PopCnt is not really an intrinsic as it has a fallback counter part and works on all platforms. Intrinsic means that it is really mapped directly to the CPU instruction without any fallbacks.

As the branch of Jeppe shows, it is pretty easy, just requires some continuous work.

How about the rest of the BMI? > TBM? AES-NI? Newer AVX?

See above.

Reply by email

Sven Barth
to J. Gareth Moreton: True, the assembler reader needs to be programmed to recognise new instructions - I…

J. Gareth Moreton schrieb am So., 17. März
2019, 22:26:

Show quoted text 10 lines
I do have a slight concern when it comes to intrinsic support, because support will be needed to be added for all platforms, (virtually) all instructions and tested to see if they expand into the expected instruction, along with determining what happens when you string several intrinsics together (i.e. passing the result of one such intrinsic into the actual parameter of another). I want to say that it will require just as much, if not more, maintenance than permitting inlined assembler routines under limited circumstances, including adding intrinsics for every individual platform's assembly language and showing that it works.

No, intrinsics can be CPU specific. So if you'd use the intrinsic for AESENC then you'd get a compile time error on non-x86 CPUs. No need to try to implement them for all platforms.

Regards,
Sven

Reply by email

J. Gareth Moreton

The main thing is the degree of control you have using pure assembler over intrinsics, and someone brought up that intrinsics don't give you good access to the FLAGS register.  Additionally, unless you do some rather untidy nested parameter chaining (calling an intrinsic and passing its result into another intrinsic, several layers deep), you don't have too much control over how the results are stored.  Normally not a terrible thing, but if you have a temporary value that you know will be discarded, you want it in a register and never stored on the stack, for example. I know you keep saying the compiler should be smart enough to determine that, but it's not.  It's not even smart enough to merge nearby "div" and "mod" instructions with the same denominator, and putting individual cases in the peephole optimizer can only go so far before you have to redesign it from the ground up (things like "full tree optimization"), and there comes a point where you can put so many possible optimisations into a compiler that it becomes prohibitively slow or take up too much memory... the main problem is that optimisation as a whole is NP-complete.  You'll never get everything, and assembly language is the only way to be sure you're making the most efficient code in specialised situations where speed is of paramount importance.

There's also the issue of maintenance... writing intrinsics for every single possible instruction on every single platform and determining that they behave in the way they should.
I guess we have been spoilt in a way because Pascal has always supported a clean and efficient way to drop into assembly language if you so choose, and this is what I've gotten used to rather than the intrinsics of C++.  I don't like the idea of putting breakpoints on the intrinsics and opening up the Disassembly window just to check that the compiler isn't blindly storing temporary values on the stack.
If I had to give one final reason... there are already functions in the RTL that are written in pure assembly language that would easily benefit being inlined, such as SwapEndian and Trunc.  Otherwise they'd have to be rewritten to use intrinsics if anyone remembers to.

There is one other thing... intrinsics haven't been merged into the trunk yet, so we can't test them or determine if they are actually what we desire.
Gareth aka. Kit

On Sun 24/03/19 09:45 , Florian Klämpfl sent:

Ben Grasset wrote 7 lines
On Sun, Mar 17, 2019 at 1:57 PM Florian Klämpfl wrote: How is it better than intrinsics support (similiar to gcc/icc etc.)? Well, it wouldn't be better than a literal equivalent to those

intriniscs, if that's what we're talking about. By which

I mean, like, say how in Clang/GCC (or languages such as Rust that use

LLVM), if you do _mm_loadl_pd or whatever, that

translates not to a function call but directly to the "inlined"

assembler instructions (at least in release builds.) 

Yes, that's what I mean. So far I have not seen a single advantage of inlining pure assembler routines over such intrinsics.

Reply by email

Florian Klämpfl
to J. Gareth Moreton
J. Gareth Moreton wrote
The main thing is the degree of control you have using pure assembler over intrinsics, and someone brought up that intrinsics don't give you good access to the FLAGS register.

Juggling with the flags is rarely possible on x86 anyways as almost all instructions change them.

Additionally, unless you do some rather untidy nested parameter chaining (calling an intrinsic and passing its result into another intrinsic, several layers deep), you don't have too much control over how the results are stored.  Normally not a terrible thing, but if you have a temporary value that you know will be discarded, you want it in a register and never stored on the stack, for example.

You do not ensure this with pure assembler either. It's even worse here: the instructions use always the same registers so the code is very prone to do a lot of spilling. Inline pure assembler routines will result in most cases in far worse code than intrinsics as the compiler cannot change the register usage in the inlined assembler.

There's also the issue of maintenance... writing intrinsics for every single possible instruction on every single platform and determining that they behave in the way they should.

Just look at the intrinsics branch, this can be easily automated.

Show quoted text 5 lines
I guess we have been spoilt in a way because Pascal has always supported a clean and efficient way to drop into assembly language if you so choose, and this is what I've gotten used to rather than the intrinsics of C++.  I don't like the idea of putting breakpoints on the intrinsics and opening up the Disassembly window just to check that the compiler isn't blindly storing temporary values on the stack.

... which will happen much more for inlined assembler as the compiler is less flexible regarding register usage.

If I had to give one final reason... there are already functions in the RTL that are written in pure assembly language that would easily benefit being inlined, such as SwapEndian and Trunc. 

Actually, trunc renders all these reasons void: it is inlined, if the code is compiled for an architecture supporting the needed instructions (i386-win32, compiled with -Cpcoreavx2 -Cavx2):

# [5] writeln(trunc(d));
call fpc_get_output
movl %eax,%ebx
fldl U_$P$PROGRAM_$$_D
fisttpq -8(%ebp)
pushl -4(%ebp)
pushl -8(%ebp)
movl %ebx,%edx
movl $0,%eax
call fpc_write_text_int64

This is not possible with inlined pure assembler routines. They would use the instruction set selected when the rtl was compiled. trunc shows perfectly why intrinsics are the way to go.

Same could be done for SwapEndian.

Show quoted text 5 lines
Otherwise they'd have to be rewritten to use intrinsics if anyone remembers to. There is one other thing... intrinsics haven't been merged into the trunk yet, so we can't test them or determine if they are actually what we desire.

This is a not a valid reason. You can play with the svn branch.

Reply by email