Sign in Join

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

Mailing lists / fpc-devel / 2019-05

Possible idea... "safe" subroutines/methods

19 messages · 6 people · –

Reply by email Subscribe to this list Your subscription

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

J. Gareth Moreton

This is something I've been thinking about for a while, and I wonder how practical it is or if it's a good idea - the ability to mark specific routines as thread-safe with a directive named "safe" or "threadsafe".

By telling the compiler that the procedure (or maybe a whole class) is thread-safe, you are telling it that you can guarantee that any objects, fields or global variables that you access are guaranteed to not suddenly change mid-routine (because another thread has modified it).  This would allow the compiler to move commonly-accessed fields into local registers or the stack for faster access, especially if the fields are only read and not written, since they'll be guaranteed to contain a constant value.

To give an example as to where this would be useful, my recent case refactoring patch - https://bugs.freepascal.org/view.php?id=35479 - the compiler would do the local storage automatically and hence I wouldn't need to write such code, which would probably count as a micro-optimisation.

Of course, by explicitly saying a procedure is thread-safe, the compiler can make no allowances when other threads actually do modify the value, potentially causing unpredictable behaviour, but because it's an explicit directive, the onus is on the programmer to ensure the routine is actually thread-safe.

For reasons of safety, the compiler should probably not consider values thread-safe across the boundary of a try...except or a try...finally block, not because of the risk of another thread modifying it, but because an interrupt might, caused by an exception.

There are still some things to work out, like how the compiler would handle a safe routine calling another routine (whether it's safe or not).

What are your thoughts?

Gareth aka. Kit

---
This email has been checked for viruses by Avast antivirus software. https://www.avast.com/antivirus

Michael Van Canneyt
to J. Gareth Moreton
J. Gareth Moreton wrote 12 lines
This is something I've been thinking about for a while, and I wonder how practical it is or if it's a good idea - the ability to mark specific routines as thread-safe with a directive named "safe" or "threadsafe". By telling the compiler that the procedure (or maybe a whole class) is thread-safe, you are telling it that you can guarantee that any objects, fields or global variables that you access are guaranteed to not suddenly change mid-routine (because another thread has modified it).  This would allow the compiler to move commonly-accessed fields into local registers or the stack for faster access, especially if the fields are only read and not written, since they'll be guaranteed to contain a constant value.

What about exceptions ?

Michael.

J. Gareth Moreton
to Michael Van Canneyt

A good question.  I did already partially answer it:

"For reasons of safety, the compiler should probably not consider values thread-safe across the boundary of a try...except or a try...finally block, not because of the risk of another thread modifying it, but because an interrupt might, caused by an exception."

When it comes to just reading non-local values, this will pose no problem, since if control is returned to the procedure, it will be in the except block and then the code that follows... different blocks.  If the non-local value is written to prior to the exception though, then it becomes a bit more complicated - one can't just write back to the global storage prior to an exception-prone call because that would be both hard to detect and very inefficient.

The safe answer would be that using local storage should only apply to non-local values that are only read, not written to. Extending it to modifying non-local values would require deeper aka. Kit

Michael Van Canneyt wrote 25 lines
On Fri, 3 May 2019, J. Gareth Moreton wrote: > This is something I've been thinking about for a while, and I wonder > how practical it is or if it's a good idea - the ability to mark > specific routines as thread-safe with a directive named "safe" or > "threadsafe". > > By telling the compiler that the procedure (or maybe a whole class) > is thread-safe, you are telling it that you can guarantee that any > objects, fields or global variables that you access are guaranteed to > not suddenly change mid-routine (because another thread has modified > it).  This would allow the compiler to move commonly-accessed fields > into local registers or the stack for faster access, especially if > the fields are only read and not written, since they'll be guaranteed > to contain a constant value. What about exceptions ? Michael. _______________________________________________ fpc-devel maillist - http://lists.freepascal.org/cgi-bin/mailman/listinfo/fpc-devel

---
This email has been checked for viruses by Avast antivirus software. https://www.avast.com/antivirus

Jonas Maebe
to J. Gareth Moreton: This is something I've been thinking about for a while, and I wonder how practical it is…
J. Gareth Moreton wrote 8 lines
By telling the compiler that the procedure (or maybe a whole class) is thread-safe, you are telling it that you can guarantee that any objects, fields or global variables that you access are guaranteed to not suddenly change mid-routine (because another thread has modified it).  This would allow the compiler to move commonly-accessed fields into local registers or the stack for faster access, especially if the fields are only read and not written, since they'll be guaranteed to contain a constant value.

Multi-threading is not the main issue. The main problems are aliasing and subroutine calls:

1) Aliasing

type
   tc = class
     a: longint;
     procedure test(var l: longint);
   end;
procedure tc.test(var l: longint);
begin
   if a5 then
     begin
       l:=1;
       // the above will change c.a to 1, but if c.a is in a register
that will not be detected
       if a1 then
         writeln('error');
     end;
end;
var
   c: tc;
begin
   c:=tc.create;
   c.a:=6;
   c.test(c.a);
   c.free;
end.

2) subroutine calls

type
   tc = class
     a: longint;
     procedure test;
   end;
var
   c: tc;
procedure change;
begin
   c.a:=1;
end;
procedure tc.test;
begin
   if a5 then
     begin
       change;
       if a1 then
         writeln('error');
     end;
end;
begin
   c:=tc.create;
   c.a:=6;
   c.test;
   c.free;
end.

In both cases, many additional scenarios are possible (there are many different way to alias memory and to perform modifications in subroutine calls).

For the former, you need inter-procedural alias analysis, or limit yourself to routines that only write to local variables. For the latter, you need to limit yourself to routines that don't call other routines, and/or record various function attributes that indicate what these other routines do. See e.g. the function attributes from LLVM (http://llvm.org/docs/LangRef.html#function-attributes) like inaccessiblememonly, inaccessiblemem_or_argmemonly, readnone, readonly, writeonly, and argmemonly. Since LLVM found a use for them in terms of optimising code, they're probably a good a start.

I wish to stress that I do _not_ propose or support adding any of those attributes to the language; most of those attributes don't exist in C/C++ either. They get added by LLVM itself while optmising and analysing the functions, or by compiler backends for auto-generted functions.

However, you could add compiler analyses that add those, or similar, attributes to the implementation procdef flags
(tprocdef.implprocoptions), and then make use of those attributes even in cross-unit calls (in case the body of the function in the other unit has already been compiled, similar to inlining). Or in case of whole-program optimisation, they could written and loaded for the entire program, so you can use them even when function bodies have not yet been parsed.

As far as the threading issue is concerned: trunk has support for the "volatile" intrinsic. At most, I would add an optimizer option that prevents optimisations that may break things in case "volatile" is missing. This should happen in very few places though, since it can only change the behaviour of a well-defined program if you are busy-waiting on a single value that another thread may change (and do nothing else with values produced by this other thread, unless you also add a bunch of memory barriers and, depending on the architecture, also acquire/release helpers).

Jonas

J. Gareth Moreton
to Jonas Maebe

This is why I posted to the group!  You can catch things that I might miss.

For the aliasing issue, I envisioned that var parameters wouldn't be affected.  Since the address is already in a register or on the stack, it's relatively efficient already.

For the subroutine call, that is indeed a little more difficult, and the compiler would have to consider that a subroutine call may modify one of the non-local values.

C/C++ was never my first language, so generally I try not to mirror it.  I even joked that seeing C-style standards in Pascal source is tantamount to colonialism!

I've noticed that my take on coding is somewhat different to others at times.  I tend not to trust the compiler to make the most efficient code.  I think the best analogy would be the difference between the Quake and the Unreal engines... Quake does all the BSP and portal building itself during a map compilation stage, while with the Unreal engine, the mapper decides where portals and the like go.  It is a tiny bit more work, but it allowed for highly detailed and optimised maps so long as you used a bit of logical thinking.

The reason why I suggested a modifier directive is for similar reasons why I'm doing the same thing with pure functions... for compilation speed.  If a function is marked as pure, the compiler will have to do a lot more processing and analysis, so if all functions are implicitly considered pure until proven otherwise, it will slow down compilation significantly.  A similar thing may happen with safe functions because it will have to undertake data flow analysis.  It's hard to say if the compiler performance hit will be significant or not, but you may be right in that safe procedures can be merged with data-flow analysis if it becomes a major part of the compiler.  The only risk is with multi-threading again - if a procedure suddenly behaves differently under the highest optimisation settings because of the lack of a 'volatile' intrinsic, I personally consider it a bug (which is why I'm not a fan of -O4 with its advertised 'may cause side-effects').

It does make for some interesting discussion though!

Gareth aka. Kit

Jonas Maebe wrote 122 lines
On 2019-05-03 19:37, J. Gareth Moreton wrote: > By telling the compiler that the procedure (or maybe a whole class) is > thread-safe, you are telling it that you can guarantee that any > objects, fields or global variables that you access are guaranteed to > not suddenly change mid-routine (because another thread has modified > it).  This would allow the compiler to move commonly-accessed fields > into local registers or the stack for faster access, especially if the > fields are only read and not written, since they'll be guaranteed to > contain a constant value. Multi-threading is not the main issue. The main problems are aliasing and subroutine calls: 1) Aliasing type   tc = class     a: longint;     procedure test(var l: longint);   end; procedure tc.test(var l: longint); begin   if a5 then     begin       l:=1;       // the above will change c.a to 1, but if c.a is in a register that will not be detected       if a1 then         writeln('error');     end; end; var   c: tc; begin   c:=tc.create;   c.a:=6;   c.test(c.a);   c.free; end. 2) subroutine calls type   tc = class     a: longint;     procedure test;   end; var   c: tc; procedure change; begin   c.a:=1; end; procedure tc.test; begin   if a5 then     begin       change;       if a1 then         writeln('error');     end; end; begin   c:=tc.create;   c.a:=6;   c.test;   c.free; end. In both cases, many additional scenarios are possible (there are many different way to alias memory and to perform modifications in subroutine calls). For the former, you need inter-procedural alias analysis, or limit yourself to routines that only write to local variables. For the latter, you need to limit yourself to routines that don't call other routines, and/or record various function attributes that indicate what these other routines do. See e.g. the function attributes from LLVM (http://llvm.org/docs/LangRef.html#function-attributes) like inaccessiblememonly, inaccessiblemem_or_argmemonly, readnone, readonly, writeonly, and argmemonly. Since LLVM found a use for them in terms of optimising code, they're probably a good a start. I wish to stress that I do _not_ propose or support adding any of those attributes to the language; most of those attributes don't exist in C/C++ either. They get added by LLVM itself while optmising and analysing the functions, or by compiler backends for auto-generted functions. However, you could add compiler analyses that add those, or similar, attributes to the implementation procdef flags (tprocdef.implprocoptions), and then make use of those attributes even in cross-unit calls (in case the body of the function in the other unit has already been compiled, similar to inlining). Or in case of whole-program optimisation, they could written and loaded for the entire program, so you can use them even when function bodies have not yet been parsed. As far as the threading issue is concerned: trunk has support for the "volatile" intrinsic. At most, I would add an optimizer option that prevents optimisations that may break things in case "volatile" is missing. This should happen in very few places though, since it can only change the behaviour of a well-defined program if you are busy-waiting on a single value that another thread may change (and do nothing else with values produced by this other thread, unless you also add a bunch of memory barriers and, depending on the architecture, also acquire/release helpers). Jonas _______________________________________________ fpc-devel maillist  -  http://lists.freepascal.org/cgi-bin/mailman/listinfo/fpc-devel

---
This email has been checked for viruses by Avast antivirus software. https://www.avast.com/antivirus

J. Gareth Moreton
to J. Gareth Moreton

Ah, slightly misinterpreted the aliasing thing... yeah, that is a danger to consider.  I'll have to think about that one.  That alone may make safe procedures, at least ones with var and out parameters, relatively impossible.

Gareth aka. Kit

J. Gareth Moreton wrote 177 lines
This is why I posted to the group!  You can catch things that I might miss. For the aliasing issue, I envisioned that var parameters wouldn't be affected.  Since the address is already in a register or on the stack, it's relatively efficient already. For the subroutine call, that is indeed a little more difficult, and the compiler would have to consider that a subroutine call may modify one of the non-local values. C/C++ was never my first language, so generally I try not to mirror it.  I even joked that seeing C-style standards in Pascal source is tantamount to colonialism! I've noticed that my take on coding is somewhat different to others at times.  I tend not to trust the compiler to make the most efficient code.  I think the best analogy would be the difference between the Quake and the Unreal engines... Quake does all the BSP and portal building itself during a map compilation stage, while with the Unreal engine, the mapper decides where portals and the like go.  It is a tiny bit more work, but it allowed for highly detailed and optimised maps so long as you used a bit of logical thinking. The reason why I suggested a modifier directive is for similar reasons why I'm doing the same thing with pure functions... for compilation speed.  If a function is marked as pure, the compiler will have to do a lot more processing and analysis, so if all functions are implicitly considered pure until proven otherwise, it will slow down compilation significantly.  A similar thing may happen with safe functions because it will have to undertake data flow analysis.  It's hard to say if the compiler performance hit will be significant or not, but you may be right in that safe procedures can be merged with data-flow analysis if it becomes a major part of the compiler.  The only risk is with multi-threading again - if a procedure suddenly behaves differently under the highest optimisation settings because of the lack of a 'volatile' intrinsic, I personally consider it a bug (which is why I'm not a fan of -O4 with its advertised 'may cause side-effects'). It does make for some interesting discussion though! Gareth aka. Kit On 04/05/2019 09:37, Jonas Maebe wrote: > On 2019-05-03 19:37, J. Gareth Moreton wrote: >> By telling the compiler that the procedure (or maybe a whole class) is >> thread-safe, you are telling it that you can guarantee that any >> objects, fields or global variables that you access are guaranteed to >> not suddenly change mid-routine (because another thread has modified >> it).  This would allow the compiler to move commonly-accessed fields >> into local registers or the stack for faster access, especially if the >> fields are only read and not written, since they'll be guaranteed to >> contain a constant value. > > Multi-threading is not the main issue. The main problems are aliasing > and subroutine calls: > > 1) Aliasing > > type >   tc = class >     a: longint; >     procedure test(var l: longint); >   end; > > procedure tc.test(var l: longint); > begin >   if a5 then >     begin >       l:=1; >       // the above will change c.a to 1, but if c.a is in a register > that will not be detected >       if a1 then >         writeln('error'); >     end; > end; > > var >   c: tc; > begin >   c:=tc.create; >   c.a:=6; >   c.test(c.a); >   c.free; > end. > > > 2) subroutine calls > > type >   tc = class >     a: longint; >     procedure test; >   end; > > var >   c: tc; > > procedure change; > begin >   c.a:=1; > end; > > procedure tc.test; > begin >   if a5 then >     begin >       change; >       if a1 then >         writeln('error'); >     end; > end; > > begin >   c:=tc.create; >   c.a:=6; >   c.test; >   c.free; > end. > > In both cases, many additional scenarios are possible (there are many > different way to alias memory and to perform modifications in > subroutine calls). > > For the former, you need inter-procedural alias analysis, or limit > yourself to routines that only write to local variables. For the > latter, you need to limit yourself to routines that don't call other > routines, and/or record various function attributes that indicate > what these other routines do. See e.g. the function attributes from > LLVM (http://llvm.org/docs/LangRef.html#function-attributes) like > inaccessiblememonly, inaccessiblemem_or_argmemonly, readnone, > readonly, writeonly, and argmemonly. Since LLVM found a use for them > in terms of optimising code, they're probably a good a start. > > I wish to stress that I do _not_ propose or support adding any of > those attributes to the language; most of those attributes don't > exist in C/C++ either. They get added by LLVM itself while optmising > and analysing the functions, or by compiler backends for > auto-generted functions. > > However, you could add compiler analyses that add those, or similar, > attributes to the implementation procdef flags > (tprocdef.implprocoptions), and then make use of those attributes > even in cross-unit calls (in case the body of the function in the > other unit has already been compiled, similar to inlining). Or in > case of whole-program optimisation, they could written and loaded for > the entire program, so you can use them even when function bodies > have not yet been parsed. > > As far as the threading issue is concerned: trunk has support for the > "volatile" intrinsic. At most, I would add an optimizer option that > prevents optimisations that may break things in case "volatile" is > missing. This should happen in very few places though, since it can > only change the behaviour of a well-defined program if you are > busy-waiting on a single value that another thread may change (and do > nothing else with values produced by this other thread, unless you > also add a bunch of memory barriers and, depending on the > architecture, also acquire/release helpers). > > > Jonas > _______________________________________________ > fpc-devel maillist  -  > http://lists.freepascal.org/cgi-bin/mailman/listinfo/fpc-devel > > --- This email has been checked for viruses by Avast antivirus software. https://www.avast.com/antivirus _______________________________________________ fpc-devel maillist  -  http://lists.freepascal.org/cgi-bin/mailman/listinfo/fpc-devel
J. Gareth Moreton
to J. Gareth Moreton

I will say though that I'm reluctant to program any kind of automatic optimisation that is not thread-safe, even with the 'volatile' intrinsic.  A 'safe' directive is a way of telling the compiler that "this routine will not be affected by multi-threaded shenanigans" so is safe to make thread-unsafe optimisations, so long as aliasing and the like doesn't catch me off-guard.

At the same time, if you're feeling very ballsy (or you know exactly what you're doing), you could mark a routine as safe so most global values benefit from potential optimisations, but the one value that is not thread-safe you can put inside a volatile intrinsic.  At least that's the plan.  The idea is that directives and modifiers only remove compiler safety, not add to it because of an uncertain optimisation.

Gareth

J. Gareth Moreton wrote 191 lines
Ah, slightly misinterpreted the aliasing thing... yeah, that is a danger to consider.  I'll have to think about that one.  That alone may make safe procedures, at least ones with var and out parameters, relatively impossible. Gareth aka. Kit On 04/05/2019 17:06, J. Gareth Moreton wrote: > This is why I posted to the group!  You can catch things that I might > miss. > > For the aliasing issue, I envisioned that var parameters wouldn't be > affected.  Since the address is already in a register or on the > stack, it's relatively efficient already. > > For the subroutine call, that is indeed a little more difficult, and > the compiler would have to consider that a subroutine call may modify > one of the non-local values. > > C/C++ was never my first language, so generally I try not to mirror > it.  I even joked that seeing C-style standards in Pascal source is > tantamount to colonialism! > > I've noticed that my take on coding is somewhat different to others > at times.  I tend not to trust the compiler to make the most > efficient code.  I think the best analogy would be the difference > between the Quake and the Unreal engines... Quake does all the BSP > and portal building itself during a map compilation stage, while with > the Unreal engine, the mapper decides where portals and the like go.  > It is a tiny bit more work, but it allowed for highly detailed and > optimised maps so long as you used a bit of logical thinking. > > The reason why I suggested a modifier directive is for similar > reasons why I'm doing the same thing with pure functions... for > compilation speed.  If a function is marked as pure, the compiler > will have to do a lot more processing and analysis, so if all > functions are implicitly considered pure until proven otherwise, it > will slow down compilation significantly.  A similar thing may happen > with safe functions because it will have to undertake data flow > analysis.  It's hard to say if the compiler performance hit will be > significant or not, but you may be right in that safe procedures can > be merged with data-flow analysis if it becomes a major part of the > compiler.  The only risk is with multi-threading again - if a > procedure suddenly behaves differently under the highest optimisation > settings because of the lack of a 'volatile' intrinsic, I personally > consider it a bug (which is why I'm not a fan of -O4 with its > advertised 'may cause side-effects'). > > It does make for some interesting discussion though! > > Gareth aka. Kit > > > On 04/05/2019 09:37, Jonas Maebe wrote: >> On 2019-05-03 19:37, J. Gareth Moreton wrote: >>> By telling the compiler that the procedure (or maybe a whole class) is >>> thread-safe, you are telling it that you can guarantee that any >>> objects, fields or global variables that you access are guaranteed to >>> not suddenly change mid-routine (because another thread has modified >>> it).  This would allow the compiler to move commonly-accessed fields >>> into local registers or the stack for faster access, especially if the >>> fields are only read and not written, since they'll be guaranteed to >>> contain a constant value. >> >> Multi-threading is not the main issue. The main problems are >> aliasing and subroutine calls: >> >> 1) Aliasing >> >> type >>   tc = class >>     a: longint; >>     procedure test(var l: longint); >>   end; >> >> procedure tc.test(var l: longint); >> begin >>   if a5 then >>     begin >>       l:=1; >>       // the above will change c.a to 1, but if c.a is in a register >> that will not be detected >>       if a1 then >>         writeln('error'); >>     end; >> end; >> >> var >>   c: tc; >> begin >>   c:=tc.create; >>   c.a:=6; >>   c.test(c.a); >>   c.free; >> end. >> >> >> 2) subroutine calls >> >> type >>   tc = class >>     a: longint; >>     procedure test; >>   end; >> >> var >>   c: tc; >> >> procedure change; >> begin >>   c.a:=1; >> end; >> >> procedure tc.test; >> begin >>   if a5 then >>     begin >>       change; >>       if a1 then >>         writeln('error'); >>     end; >> end; >> >> begin >>   c:=tc.create; >>   c.a:=6; >>   c.test; >>   c.free; >> end. >> >> In both cases, many additional scenarios are possible (there are >> many different way to alias memory and to perform modifications in >> subroutine calls). >> >> For the former, you need inter-procedural alias analysis, or limit >> yourself to routines that only write to local variables. For the >> latter, you need to limit yourself to routines that don't call other >> routines, and/or record various function attributes that indicate >> what these other routines do. See e.g. the function attributes from >> LLVM (http://llvm.org/docs/LangRef.html#function-attributes) like >> inaccessiblememonly, inaccessiblemem_or_argmemonly, readnone, >> readonly, writeonly, and argmemonly. Since LLVM found a use for them >> in terms of optimising code, they're probably a good a start. >> >> I wish to stress that I do _not_ propose or support adding any of >> those attributes to the language; most of those attributes don't >> exist in C/C++ either. They get added by LLVM itself while optmising >> and analysing the functions, or by compiler backends for >> auto-generted functions. >> >> However, you could add compiler analyses that add those, or similar, >> attributes to the implementation procdef flags >> (tprocdef.implprocoptions), and then make use of those attributes >> even in cross-unit calls (in case the body of the function in the >> other unit has already been compiled, similar to inlining). Or in >> case of whole-program optimisation, they could written and loaded >> for the entire program, so you can use them even when function >> bodies have not yet been parsed. >> >> As far as the threading issue is concerned: trunk has support for >> the "volatile" intrinsic. At most, I would add an optimizer option >> that prevents optimisations that may break things in case "volatile" >> is missing. This should happen in very few places though, since it >> can only change the behaviour of a well-defined program if you are >> busy-waiting on a single value that another thread may change (and >> do nothing else with values produced by this other thread, unless >> you also add a bunch of memory barriers and, depending on the >> architecture, also acquire/release helpers). >> >> >> Jonas >> _______________________________________________ >> fpc-devel maillist  -  >> http://lists.freepascal.org/cgi-bin/mailman/listinfo/fpc-devel >> >> > > --- > This email has been checked for viruses by Avast antivirus software. > https://www.avast.com/antivirus > > _______________________________________________ > fpc-devel maillist  -  > http://lists.freepascal.org/cgi-bin/mailman/listinfo/fpc-devel > > _______________________________________________ fpc-devel maillist  -  http://lists.freepascal.org/cgi-bin/mailman/listinfo/fpc-devel
J. Gareth Moreton
to J. Gareth Moreton

Thinking about it more, I can see "safe" as a procedure directive not working because there are so many contrived cases that can easily break it, and covering all those cases will make such a feature pretty much useless.

Talking about attributes, I think a better way is to mark specific variables as "safe" or "involatile", which ties in well with the proposals for the "volatile" attribute, I believe, since it will be in effect completely opposite to a volatile variable. var parameters will still be the most difficult problem to work out - one solution would be to forbid passing nonvolatile variables into var parameters, but this might seem unnecessarily picky and it doesn't solve tricks with pointers.

I figure data-flow analysis will address everything eventually, but I can't promise the compiler won't become very inefficient as a result.

Gareth aka. Kit

---
This email has been checked for viruses by Avast antivirus software. https://www.avast.com/antivirus

Ben Grasset
to J. Gareth Moreton
PM J. Gareth Moreton wrote
I figure data-flow analysis will address everything eventually, but I can't promise the compiler won't become very inefficient as a result.

I mean no offense by this, and note that I think you've done more for FPC in the last year (or two years-ish maybe?) than many people ever have, but I must say that to me this kind of thinking is very wrong and a big, specific part of what holds FPC back. It's as though people constantly forget that *not everything* needs to be some immutable hardcoded part of the overall compilation process, permanently affecting everyone until the end of time. In reality you can implement *literally anything at all*, and even if it takes seven hours to run, as long as it is an opt-in optimization enabled via a command-line argument or compiler directive there's no logical reason for anyone to complain about it because, again, it's *opt-in.*.

And don't even get me started on the subject of "people who care in any way whatsoever about the size of the FPC executable, even though they are running desktop hardware and a desktop OS, *not* embedded hardware with an embedded OS, and so it could not possibly matter in *any* practical way to them even if the FPC executable was 25x larger than it currently is."

J. Gareth Moreton
to Ben Grasset

You may be right.  Part of Pascal's legacy through Turbo Pascal, Delphi and Free Pascal is its exceptionally fast compiler.  For the more complex optimisations like data-flow analysis and what I call a "deep optimiser", there's nothing wrong with it taking a long time (so long as it is not weeks!) just, as you said, you opt in and enable "highest level optimisation", so the user knows what they're getting themselves in for.

Some elements of data flow analysis are a little more difficult, like with this idea of moving some global variables into more local storage so as to improve performance, because there may be some sneaky tricks done with pointers, var parameters or even assembly language that will break the compiler's assumption.

One problem I do have with FPC and its optimiser is that -O4 is specifically said that it may cause side-effects and even tongue-in-cheek says "beware".  To me, this is bad design because it discourages the end users from using the option and if an optimisation causes well-behaved code to suddenly start malfunctioning, then that is a bug in my eyes.  To me, I'd like -O4 to be that aggressive, deep optimisation that may take a few hours on the largest of projects (although in reality it will probably only be a few minutes), but which isn't going to create something questionable that will break things.

It's partly why I was wondering about marking certain procedures or variables as non-volatile so they can be moved to local storage without having to do it programmatically and making the code unmaintainable.  By specifically telling the compiler "this value is thread-safe", it can make those assumptions because you've assured it that you're not doing something tricky with it, rather than attempting to let the compiler work it out for itself (which is probably impossible) and hence assume it's not thread-safe and hence volatile.

I will admit that I am one of those people who likes to reduce code size where possible, but not at the expense of performance. My idea of reducing code size comes from using more efficient encodings for certain things (my first patch contribution changed "MOV RAX, x", where x < 2^32, to "MOV EAX, x", saving between 4 and 5 bytes each time, depending on the register, reducing the compiler's EXE size by 50 Kb) and stripping out dead code.

Gareth aka. Kit

Ben Grasset wrote 29 lines
On Sat, May 4, 2019 at 5:31 PM J. Gareth Moreton < > wrote: I figure data-flow analysis will address everything eventually, but I can't promise the compiler won't become very inefficient as a result. I mean no offense by this, and note that I think you've done more for FPC in the last year (or two years-ish maybe?) than many people ever have, but I must say that to me this kind of thinking is very wrong and a big, specific part of what holds FPC back. It's as though people constantly forget that *not everything* needs to be some immutable hardcoded part of the overall compilation process, permanently affecting everyone until the end of time. In reality you can implement *literally anything at all*, and even if it takes seven hours to run, as long as it is an opt-in optimization enabled via a command-line argument or compiler directive there's no logical reason for anyone to complain about it because, again, it's *opt-in.*. And don't even get me started on the subject of "people who care in any way whatsoever about the size of the FPC executable, even though they are running desktop hardware and a desktop OS, *not* embedded hardware with an embedded OS, and so it could not possibly matter in *any* practical way to them even if the FPC executable was 25x larger than it currently is." _______________________________________________ fpc-devel maillist - http://lists.freepascal.org/cgi-bin/mailman/listinfo/fpc-devel

---
This email has been checked for viruses by Avast antivirus software. https://www.avast.com/antivirus

Sven Barth
to J. Gareth Moreton

J. Gareth Moreton schrieb am So., 5. Mai 2019,
05:11:

Show quoted text 10 lines
One problem I do have with FPC and its optimiser is that -O4 is specifically said that it may cause side-effects and even tongue-in-cheek says "beware". To me, this is bad design because it discourages the end users from using the option and if an optimisation causes well-behaved code to suddenly start malfunctioning, then that is a bug in my eyes. To me, I'd like -O4 to be that aggressive, deep optimisation that may take a few hours on the largest of projects (although in reality it will probably only be a few minutes), but which isn't going to create something questionable that will break things.

The "beware" is only related to code that relies on implementation details. E.g. the order of fields in a class which everyone assumes is the same in memory as the one its written in the code.

It's partly why I was wondering about marking certain procedures or

Show quoted text 8 lines
variables as non-volatile so they can be moved to local storage without having to do it programmatically and making the code unmaintainable. By specifically telling the compiler "this value is thread-safe", it can make those assumptions because you've assured it that you're not doing something tricky with it, rather than attempting to let the compiler work it out for itself (which is probably impossible) and hence assume it's not thread-safe and hence volatile.

And you can get malfunctioning with such modifiers as well, namely if the user does not adhere to what to what they told the compiler. This might even happen by accident if one uses third party code and does not know that the code uses that (the known problems with "const" are an existing example of that).

Regards,
Sven

Jonas Maebe
to J. Gareth Moreton: This is why I posted to the group!  You can catch things that I might miss. For the…
J. Gareth Moreton wrote
I've noticed that my take on coding is somewhat different to others at times.  I tend not to trust the compiler to make the most efficient code.

You generally shouldn't, and it indeed generally won't do it either. That's why even with the most advanced optimising compilers out there, some low-level and performance-critical routines like memcpy are still hand-coded in assembly on all platforms.

However, whenever the compiler can figure something out by itself without intervention from the programmer, no matter how small, there are several advantages:
1) It has a much lower risk of introducing bugs. The analysis/transformation can of course have bugs, but once it's stable, it should be good for a long time. Conversely, while a programmer may have a good idea of how a routine works when they have just written it, code and tends to change, and at some point the modifiers are probably going to get out of sync with what the code does.
2) It will be applied much wider. You don't need people to go through entire bodies of existing code to look which modifier could help where and add them. The compiler will just do everything automatically, even for old legacy code that no one has looked at in years (and want to keep it that way).
3) Programmers can focus more on function rather than performance. This generally leads to clearer and more maintainable code, which improves productivity.

Jonas

Jonas Maebe
to Marģers .
Marģers . wrote 5 lines
As i understand this idea is another way around key word "volatile" in order to allow compiler perform more optimization. Then why to go half way introducing "safe", when it's better introduce "volatile". Not too long ago here was discussion about it, but it was strongly rejected by core team.

As mentioned in a previous message, fpc trunk supports a volatile intrinsic:
http://wiki.freepascal.org/FPC_New_Features_Trunk#Support_for_.22volatile.22_intrinsic

Jonas

Marģers .
to Jonas Maebe
As mentioned in a previous message, fpc trunk

supports a volatile

intrinsic:

http://wiki.freepascal.org/FPC_New_Features_Trunk#Support_for_.22volatile.22_intrinsic

My bad, I didn't know about volatile intrinsic.
So, does it mean that compiler is allowed to
optimize any variable, even promote global
variables to registers, as long they are not in
volatile(...)?

Jonas _______________________________________________ fpc-devel maillist -

http://lists.freepascal.org/cgi-bin/mailman/listinfo/fpc-devel

Jonas Maebe
to Marģers .
Marģers . wrote 11 lines
> As mentioned in a previous message, fpc trunk supports a volatile > intrinsic: > http://wiki.freepascal.org/FPC_New_Features_Trunk#Support_for_.22volatile.22_intrinsic My bad, I didn't know about volatile intrinsic. So, does it mean that compiler is allowed to optimize any variable, even promote global variables to registers, as long they are not in volatile(...)?

The compiler would also need to prove that no writes can happen through aliases for the duration that they are kept in registers. It has no support for this yet.

LLVM does have support for this though, and does perform such optimisations on code generated by FPC via the LLVM backend.

Jonas

J. Gareth Moreton
to Jonas Maebe

I'd say the people have spoken.  No "safe"!  Don't worry, I'm not going to get angry or anything... I'm appreciating the discussion we're having.

For the volatile intrinsic, was there talk of "volatile" being an attribute as well? I'm guessing this means declaring something as, for example "var FTerminated: Boolean volatile;"

My fear with allowing the promotion of global variables to registers is that it will break old code written when the "volatile" intrinsic didn't exist.  Then again, it can simply be an option that is only present in -O4 or if the programmer specifically asks for it (and maybe as an option in Lazarus' "Project Options" dialog).

Maybe it already works this way, but I would like to see -O4 work under all general-purpose circumstances where code doesn't use any implementation-specific practices like trying to access a field by a hard-coded pointer offset or using an unsafe or questionable typecast (e.g. typecasting a pointer to a LongInt as a PWord) that would trick most compiler checks.

If a volatile attribute existed for variables, then the compiler could throw a warning if you pass it into a routine's var parameter.  If there's no upper limit on how long -O4 would take to compile, then the compiler can do things like keep track on what volatile variables a procedure accesses and be more selective in when such a warning is thrown.

Originally, the "safe" proposal was so I didn't have to flood the code with manual instances of promoting global variables into local registers.  To quote my original example -
https://bugs.freepascal.org/view.php?id=35479 - I promote the domain's lower and upper bounds to object fields so one doesn't have to call "getrange" more than once (it makes a notable saving).  At the same time, I only write to the fields once the code has finished using them in expressions (setting "jumptable_no_range").  Then in the actual jump table creation routine, it reads those fields into local variables so there's no longer a need to perform constant pointer dereferences.  It's a subtle speed-up that is largely overshadowed by the removal of "getrange", but doing this too much risks the code becoming an unwieldly mess, as well as cases of flawed programmer logic when they copy a global variable to a local one, but then it's only read once.  I suppose the answer to that one is to make sure the "volatile" intrinsic/attribute is well-documented, especially mentioning it if such optimisations become part of -O4, say.

Gareth aka. Kit

Jonas Maebe wrote 28 lines
On 05/05/2019 15:31, Marģers . via fpc-devel wrote: >> As mentioned in a previous message, fpc trunk > supports a volatile >> intrinsic: >> > http://wiki.freepascal.org/FPC_New_Features_Trunk#Support_for_.22volatile.22_intrinsic > > > My bad, I didn't know about volatile intrinsic. > So, does it mean that compiler is allowed to > optimize any variable, even promote global > variables to registers, as long they are not in > volatile(...)? The compiler would also need to prove that no writes can happen through aliases for the duration that they are kept in registers. It has no support for this yet. LLVM does have support for this though, and does perform such optimisations on code generated by FPC via the LLVM backend. Jonas _______________________________________________ fpc-devel maillist  -  http://lists.freepascal.org/cgi-bin/mailman/listinfo/fpc-devel

---
This email has been checked for viruses by Avast antivirus software. https://www.avast.com/antivirus

Jonas Maebe
to J. Gareth Moreton
J. Gareth Moreton wrote
For the volatile intrinsic, was there talk of "volatile" being an attribute as well? I'm guessing this means declaring something as, for example "var FTerminated: Boolean volatile;"

See
http://wiki.freepascal.org/FPC_New_Features_Trunk#Support_for_.22volatile.22_intrinsic and the linked article from that entry for more about that.

My fear with allowing the promotion of global variables to registers is that it will break old code written when the "volatile" intrinsic didn't exist.

There is very little thread-safe code that can be broken by this change. The reason is that just waiting on a global variable to change its value is not nearly enough to synchronise two threads, unless they don't exchange any other data besides that one single variable. You need memory barriers and on certain architectures also acquire/release, all of which cannot be analysed by the compiler and hence it will have to assume that all global data may have changed across them (and hence it will reload all global data from memory).

Additionally, spinning on a volatile variable (without calling "yield" or something similar in the loop, which would stop this change from breaking anything) is about the most inefficient way in existence to synchronise two threads.

Then again, it can simply be an option that is only present in -O4 or if the programmer specifically asks for it (and maybe as an option in Lazarus' "Project Options" dialog).

That is not possible for 's just a generic optimisation there. I don't consider this to be a big problem, because proper multi-threading code won't be affected by it. Broken code will also break in a very obvious and easy to locate way: it will just sit in an endless loop at the location where the loop was.

Jonas

J. Gareth Moreton
to Jonas Maebe

Thanks so much for the insight Jonas.  Heh, Linus is as passionate as I am sometimes... volatile is evil and all!  It makes sense though... and it will encourage people to write better code.

The 'spinning on a global variable' I admit I've used.  In my case it was in a slave thread that did some number crunching in a long loop, and if the main program wanted to close, it would set the "Terminate" flag, which would make the loop exit and do some clean-up before the slave thread wraps up.  My concern was that checking an implementation-specific semaphore would be prohibitively inefficient in a tight loop.

I think as long as "volatile" is well-documented, especially on topics of multi-threaded code, then everything should be fine.

To use the compiler again as an example, once place where I can see advantages of moving values to local registers is with the "TmpUsedRegs" field, which is a pooled object that is created when the optimiser object is instantiated, and is used by the peephole optimiser when it wishes to look ahead with register usage (originally, TmpUsedRegs was created and destoyed if and when it was needed, but I argued to make it a pooled object in order to remove excess allocation and deallocation, and also improved maintenance because a couple of the methods forgot to free TmpUsedRegs afterwards, causing a memory leak).  The internal pointer never changes, and all the var parameters of functions like OptPass1MOV contain an incompatible class, so unless you do something crazy with typecasting, moving TmpUsedRegs to a local register or stack value can only provide a speed gain (and if you do crazy thing with typecasting, you're likely to case an access violation anyway).

So if I was to start researching the promotion of global values into local registers or, at the very least, the stack, should it still be an -O4 option, or is it safe enough to promote to -O3, so long as things like var parameters are carefully checked?  For the deepest of optimisations, it might get slow, but is this acceptable for -O3 and -O4?

Gareth aka. Kit

Jonas Maebe wrote 45 lines
On 05/05/2019 19:34, J. Gareth Moreton wrote: > For the volatile intrinsic, was there talk of "volatile" being an > attribute as well? I'm guessing this means declaring something as, > for example "var FTerminated: Boolean volatile;" See http://wiki.freepascal.org/FPC_New_Features_Trunk#Support_for_.22volatile.22_intrinsic and the linked article from that entry for more about that. > My fear with allowing the promotion of global variables to registers > is that it will break old code written when the "volatile" intrinsic > didn't exist. There is very little thread-safe code that can be broken by this change. The reason is that just waiting on a global variable to change its value is not nearly enough to synchronise two threads, unless they don't exchange any other data besides that one single variable. You need memory barriers and on certain architectures also acquire/release, all of which cannot be analysed by the compiler and hence it will have to assume that all global data may have changed across them (and hence it will reload all global data from memory). Additionally, spinning on a volatile variable (without calling "yield" or something similar in the loop, which would stop this change from breaking anything) is about the most inefficient way in existence to synchronise two threads. >   Then again, it can simply be an option that is only present in -O4 > or if the programmer specifically asks for it (and maybe as an option > in Lazarus' "Project Options" dialog). That is not possible for 's just a generic optimisation there. I don't consider this to be a big problem, because proper multi-threading code won't be affected by it. Broken code will also break in a very obvious and easy to locate way: it will just sit in an endless loop at the location where the loop was. Jonas _______________________________________________ fpc-devel maillist  -  http://lists.freepascal.org/cgi-bin/mailman/listinfo/fpc-devel

---
This email has been checked for viruses by Avast antivirus software. https://www.avast.com/antivirus