Sign in Join

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

Mailing lists / fpc-devel / 2022-04

Thoughts: Make FillChar etc. an intrinsic for specialised performance potential

16 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

Hi everyone,

This is something that sprung to mind when thinking about code speed and the like, and one thing that cropped up is the initialisation of large variables such as arrays or records.  A common means of doing this is, say:

FillChar(MyVar, SizeOf(MyVar), 0);

To keep things as general-purpose as possible, this usually results in a function call that decides the best course of action, and for very large blocks of data whose size may not be deterministic (e.g. a file buffer), this is the best approach - the overhead is relatively small and it quickly uses fast block-move instructions.

However, for small-to-mid-sized variables of known size, this can lead to some inefficiencies, first by not taking into account that the size of the variable is known, but also because the initialisation value is zero, more often that not, and the variable is probably aligned on the stack (so the checks to make sure a pointer is aligned are unnecessary).

I did a proof of concept on x86_64-win64 with the following record:

type
   TTestRecord = record
     Field1: Byte;
     Field2, Field3, Field4: Integer;
   end;

SizeOf(TTestRecord) is 16 and all the fields are on 4-byte boundaries.  Nothing particularly special.

I then declared a variable of this time and filled the fields with random values, and then ran two different methods to clear their memory.  To get a good speed average, I ran each method 1,000,000,000 times in a for-loop.  The first method was:

FillChar(TestRecord, SizeOf(TestRecord), 0);

The second method was inline assembly language (which I've called 'the intrinsic'):

asm
  PXOR   XMM0, XMM0
  MOVDQU [RIP+TestRecord], XMM0
end;2

It's not perfect because the presence of inline assembly prevents the use of register variables (although TestRecord is always on the stack regardless), but the performance hit is barely noticeable in this case, and if the assembly language were inserted by the compiler, the register variable problem won't arise.

These are my results:

 FillChar time: 2.398 ns

Field1 = 0
Field2 = 0
Field3 = 0
Field4 = 0

Intrinsic time: 1.336 ns

Field1 = 0
Field2 = 0
Field3 = 0
Field4 = 0

Sure, it's on the order of nanoseconds, but the intrinsic is almost twice as fast.

In terms of size - FillChar call = 20 bytes:

488d0d22080200           lea 0x20822(%rip),%rcx        # 0x100022010 4531c0                   xor    %r8d,%r8d ba10000000               mov    $0x10,%edx e8150a0000               callq  0x100002210 <SYSTEM_$$_FILLCHAR$formal$INT64$BYTE>

The intrinsic = 12 bytes:

660fefc0                 pxor %xmm0,%xmm0 f30f7f05bd050200         movdqu %xmm0,0x205bd(%rip)        # 0x100022010

For a 32-byte record instead, an extra 8-byte MOVDQU instruction would be required, so the 2 would be equal size, but with the bonus that the intrinsic doesn't have a function call and will probably help optimisation in the rest of the procedure by freeing up the registers used to pass parameters (%rcx, %rdx and %r8 in this case; although the intrinsic will require an MM register in this x86_64 example, they tend to not be used as often).  Also, the peephole optimizer can remove redundant PXOR XMM0, XMM0 calls, which will help as well if there are multiple FillChar calls.

I'm not proposing a total rewrite, and I would say that in the default case, it should just fall back to the in-built System functions, but the relevant compiler nodes could be overridden on specific platforms to generate smaller, more optimised code when the sizes and values are known at compile time.

Now, in this example, it is still faster to simply set the fields manually one-by-one (clocks in at around 1.2 ns), possibly due to the unaligned write (MOVDQU) and internal SSE state switching adding some overhead, but there's nothing to stop the compiler from inserting code in place of the FillChar call to do just that if it thinks it's the fastest method.  Then again, one has to be a little bit careful because FillChar and the intrinsic will also set the filler bytes between Field1 and Field2 to 0, whereas manually assigning 0 to the fields won't (so they aren't strictly equivalent and might only be allowed if there are no filler bytes or when compiling under -O4, but the latter may still be dangerous when typecasting is concerned), and extra care would have to be taken when unions are concerned (sorry, 'union' that's a C term - what's the official Pascal term again?).

Actual Pascal calls to FillChar would not change in any way and so theoretically it won't break existing code.  The only drawback is that the intrinsic and the internal System functions would have to be named the same so constructs such as "FuncPtr := @FillChar;" as well as calling FillChar from assembler routines stilll work, and the compiler would have to know how to differentiate between the two.

Just on the surface, what are your thoughts?

Garetha ka. Kit

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

Reply by email

Benito van der Zander
to J. Gareth Moreton

Hi,

it could always inline it.

For small sizes do that mov and for large sizes do rep stosb on x86. It is very fast nowadays. Faster than FillChar on my Intel laptop. (except for mid sizes like 128 bytes)

Bye,
Benito

J. Gareth Moreton wrote 127 lines
Hi everyone, This is something that sprung to mind when thinking about code speed and the like, and one thing that cropped up is the initialisation of large variables such as arrays or records.  A common means of doing this is, say: FillChar(MyVar, SizeOf(MyVar), 0); To keep things as general-purpose as possible, this usually results in a function call that decides the best course of action, and for very large blocks of data whose size may not be deterministic (e.g. a file buffer), this is the best approach - the overhead is relatively small and it quickly uses fast block-move instructions. However, for small-to-mid-sized variables of known size, this can lead to some inefficiencies, first by not taking into account that the size of the variable is known, but also because the initialisation value is zero, more often that not, and the variable is probably aligned on the stack (so the checks to make sure a pointer is aligned are unnecessary). I did a proof of concept on x86_64-win64 with the following record: type   TTestRecord = record     Field1: Byte;     Field2, Field3, Field4: Integer;   end; SizeOf(TTestRecord) is 16 and all the fields are on 4-byte boundaries.  Nothing particularly special. I then declared a variable of this time and filled the fields with random values, and then ran two different methods to clear their memory.  To get a good speed average, I ran each method 1,000,000,000 times in a for-loop.  The first method was: FillChar(TestRecord, SizeOf(TestRecord), 0); The second method was inline assembly language (which I've called 'the intrinsic'): asm   PXOR   XMM0, XMM0   MOVDQU [RIP+TestRecord], XMM0 end;2 It's not perfect because the presence of inline assembly prevents the use of register variables (although TestRecord is always on the stack regardless), but the performance hit is barely noticeable in this case, and if the assembly language were inserted by the compiler, the register variable problem won't arise. These are my results:  FillChar time: 2.398 ns Field1 = 0 Field2 = 0 Field3 = 0 Field4 = 0 Intrinsic time: 1.336 ns Field1 = 0 Field2 = 0 Field3 = 0 Field4 = 0 Sure, it's on the order of nanoseconds, but the intrinsic is almost twice as fast. In terms of size - FillChar call = 20 bytes: 488d0d22080200           lea 0x20822(%rip),%rcx        # 0x100022010 4531c0                   xor    %r8d,%r8d ba10000000               mov    $0x10,%edx e8150a0000               callq  0x100002210 <SYSTEM_$$_FILLCHAR$formal$INT64$BYTE> The intrinsic = 12 bytes: 660fefc0                 pxor %xmm0,%xmm0 f30f7f05bd050200         movdqu %xmm0,0x205bd(%rip)        # 0x100022010 For a 32-byte record instead, an extra 8-byte MOVDQU instruction would be required, so the 2 would be equal size, but with the bonus that the intrinsic doesn't have a function call and will probably help optimisation in the rest of the procedure by freeing up the registers used to pass parameters (%rcx, %rdx and %r8 in this case; although the intrinsic will require an MM register in this x86_64 example, they tend to not be used as often).  Also, the peephole optimizer can remove redundant PXOR XMM0, XMM0 calls, which will help as well if there are multiple FillChar calls. I'm not proposing a total rewrite, and I would say that in the default case, it should just fall back to the in-built System functions, but the relevant compiler nodes could be overridden on specific platforms to generate smaller, more optimised code when the sizes and values are known at compile time. Now, in this example, it is still faster to simply set the fields manually one-by-one (clocks in at around 1.2 ns), possibly due to the unaligned write (MOVDQU) and internal SSE state switching adding some overhead, but there's nothing to stop the compiler from inserting code in place of the FillChar call to do just that if it thinks it's the fastest method.  Then again, one has to be a little bit careful because FillChar and the intrinsic will also set the filler bytes between Field1 and Field2 to 0, whereas manually assigning 0 to the fields won't (so they aren't strictly equivalent and might only be allowed if there are no filler bytes or when compiling under -O4, but the latter may still be dangerous when typecasting is concerned), and extra care would have to be taken when unions are concerned (sorry, 'union' that's a C term - what's the official Pascal term again?). Actual Pascal calls to FillChar would not change in any way and so theoretically it won't break existing code.  The only drawback is that the intrinsic and the internal System functions would have to be named the same so constructs such as "FuncPtr := @FillChar;" as well as calling FillChar from assembler routines stilll work, and the compiler would have to know how to differentiate between the two. Just on the surface, what are your thoughts? Garetha ka. Kit

Reply by email

Sven Barth
to Benito van der Zander

Benito van der Zander via fpc-devel
schrieb am Sa., 16. Apr. 2022, 15:43:

Show quoted text 8 lines
Hi, it could always inline it. For small sizes do that mov and for large sizes do rep stosb on x86. It is very fast nowadays. Faster than FillChar on my Intel laptop. (except for mid sizes like 128 bytes)

FillChar is on most platforms an assembly function and FPC *never* inlines assembly functions.

Regards,
Sven

Reply by email

Sven Barth
to J. Gareth Moreton: This is something that sprung to mind when thinking about code speed and the like, and…

J. Gareth Moreton via fpc-devel schrieb am
Sa., 16. Apr. 2022, 01:33:

Show quoted text 7 lines
Actual Pascal calls to FillChar would not change in any way and so theoretically it won't break existing code. The only drawback is that the intrinsic and the internal System functions would have to be named the same so constructs such as "FuncPtr := @FillChar;" as well as calling FillChar from assembler routines stilll work, and the compiler would have to know how to differentiate between the two.

Intrinsic symbols do not participate in overload selection and you can't have both an intrinsic and a function symbol in the same unit. The construct you showed thus *will* fail.

Note: this is not necessarily an argument against doing this, cause with 3.2.0 we did the same with Insert and Delete when they were extended to support dynamic arrays as well, but it needs to be kept in mind.

Regards,
Sven

Reply by email

Jeppe Johansen
to Sven Barth: Benito van der Zander via fpc-devel schrieb am Sa., 16. Apr. 2022, 15:43: FillChar is on…
Sven Barth wrote 14 lines
Benito van der Zander via fpc-devel schrieb am Sa., 16. Apr. 2022, 15:43: Hi, it could always inline it. For small sizes do that mov and for large sizes do rep stosb on x86. It is very fast nowadays. Faster than FillChar on my Intel laptop. (except for mid sizes like 128 bytes) FillChar is on most platforms an assembly function and FPC *never* inlines assembly functions.

But having it as an intrinsic would allow the CG to generate optimal sequences for certain cases, and otherwise fall back to the inherited method which just inserts a call to the RTL Fill* procedures.

Reply by email

Benito van der Zander
to Sven Barth: Benito van der Zander via fpc-devel schrieb am Sa., 16. Apr. 2022, 15:43: FillChar is on…

Hi,

FillChar is on most platforms an assembly function and FPC *never* inlines assembly functions.

even without inlining, rep stosb is faster than FillChar:

 {$asmmode intel}
procedure repfillchar(var buffer; len: sizeuint; c: byte); begin
  asm
    mov al, c
    mov rdi, buffer
    mov rcx, len
    rep stosb
  end;
end;

1GB:
fillchar: 364 ms
repfillchar 321 ms

4GB:
fillchar: 1382 ms
repfillchar:1217 ms

Bye,
Benito

Reply by email

Florian Klämpfl
to J. Gareth Moreton: This is something that sprung to mind when thinking about code speed and the like, and…
Show quoted text 74 lines
Am 16.04.2022 um 01:26 schrieb J. Gareth Moreton via fpc-devel : Hi everyone, This is something that sprung to mind when thinking about code speed and the like, and one thing that cropped up is the initialisation of large variables such as arrays or records. A common means of doing this is, say: FillChar(MyVar, SizeOf(MyVar), 0); To keep things as general-purpose as possible, this usually results in a function call that decides the best course of action, and for very large blocks of data whose size may not be deterministic (e.g. a file buffer), this is the best approach - the overhead is relatively small and it quickly uses fast block-move instructions. However, for small-to-mid-sized variables of known size, this can lead to some inefficiencies, first by not taking into account that the size of the variable is known, but also because the initialisation value is zero, more often that not, and the variable is probably aligned on the stack (so the checks to make sure a pointer is aligned are unnecessary). I did a proof of concept on x86_64-win64 with the following record: type TTestRecord = record Field1: Byte; Field2, Field3, Field4: Integer; end; SizeOf(TTestRecord) is 16 and all the fields are on 4-byte boundaries. Nothing particularly special. I then declared a variable of this time and filled the fields with random values, and then ran two different methods to clear their memory. To get a good speed average, I ran each method 1,000,000,000 times in a for-loop. The first method was: FillChar(TestRecord, SizeOf(TestRecord), 0); The second method was inline assembly language (which I've called 'the intrinsic'): asm PXOR XMM0, XMM0 MOVDQU [RIP+TestRecord], XMM0 end;2 It's not perfect because the presence of inline assembly prevents the use of register variables (although TestRecord is always on the stack regardless), but the performance hit is barely noticeable in this case, and if the assembly language were inserted by the compiler, the register variable problem won't arise. These are my results: FillChar time: 2.398 ns Field1 = 0 Field2 = 0 Field3 = 0 Field4 = 0 Intrinsic time: 1.336 ns Field1 = 0 Field2 = 0 Field3 = 0 Field4 = 0 Sure, it's on the order of nanoseconds, but the intrinsic is almost twice as fast. In terms of size - FillChar call = 20 bytes: 488d0d22080200 lea 0x20822(%rip),%rcx # 0x100022010 4531c0 xor %r8d,%r8d ba10000000 mov $0x10,%edx e8150a0000 callq 0x100002210 <SYSTEM_$$_FILLCHAR$formal$INT64$BYTE> The intrinsic = 12 bytes: 660fefc0 pxor %xmm0,%xmm0 f30f7f05bd050200 movdqu %xmm0,0x205bd(%rip) # 0x100022010 For a 32-byte record instead, an extra 8-byte MOVDQU instruction would be required, so the 2 would be equal size, but with the bonus that the intrinsic doesn't have a function call and will probably help optimisation in the rest of the procedure by freeing up the registers used to pass parameters (%rcx, %rdx and %r8 in this case; although the intrinsic will require an MM register in this x86_64 example, they tend to not be used as often). Also, the peephole optimizer can remove redundant PXOR XMM0, XMM0 calls, which will help as well if there are multiple FillChar calls. I'm not proposing a total rewrite, and I would say that in the default case, it should just fall back to the in-built System functions, but the relevant compiler nodes could be overridden on specific platforms to generate smaller, more optimised code when the sizes and values are known at compile time. Now, in this example, it is still faster to simply set the fields manually one-by-one (clocks in at around 1.2 ns), possibly due to the unaligned write (MOVDQU) and internal SSE state switching adding some overhead, but there's nothing to stop the compiler from inserting code in place of the FillChar call to do just that if it thinks it's the fastest method. Then again, one has to be a little bit careful because FillChar and the intrinsic will also set the filler bytes between Field1 and Field2 to 0, whereas manually assigning 0 to the fields won't (so they aren't strictly equivalent and might only be allowed if there are no filler bytes or when compiling under -O4, but the latter may still be dangerous when typecasting is concerned), and extra care would have to be taken when unions are concerned (sorry, 'union' that's a C term - what's the official Pascal term again?). Actual Pascal calls to FillChar would not change in any way and so theoretically it won't break existing code. The only drawback is that the intrinsic and the internal System functions would have to be named the same so constructs such as "FuncPtr := @FillChar;" as well as calling FillChar from assembler routines stilll work, and the compiler would have to know how to differentiate between the two. Just on the surface, what are your thoughts?

Inlining FillChar is for sure useful (same for move). The FillChar in the system unit could stay, the compiler could just replace a call to System.FillChar by some compiler generated assembler doing the FillChar.

Reply by email

J. Gareth Moreton
to Florian Klämpfl

It's funny - for some reason I was expecting a lot of opposition!

I knew about FillChar being written in assembly langauge and know from experience that FPC will never support the inlining of pure assembler routines (as Florian said, it will just open a huge can of worms).  My thought was how to get around the intrinsic and procedure having the same name, since I can't just rename the procedure to, say, _fpc_fillchar because that will break backward compatibility in some situations.

While I'm a little wary about hard-coded constructs like "if proc = System.FillChar then ReplaceWithIntrinsic" since it seems inflexible, this is probably one of those times that it's justified, and as people have said, it's been done with Insert and Delete aready.

Even if inlining or pure functions were a thing (that's still on my agenda), it wouldn't work with FillChar due to the assembly language.  Also, things like checking a variable's memory alignment wouldn't be able to be optimised out without additional information from the compiler that's generally only available at the node level (in the compiler, a type's base alignment is a property you can very quickly retrieve, without having to go through the mess of checking to see if the base register is RSP and the offset some multiple of 8 or something).

Sounds like something to start experimenting with!

Gareth aka. Kit

P.S. I also worked out that the reason why PXOR; MOVDQU is slower than just setting the fields manually is also because there's a pipeline stall between PXOR and MOVDQU, whereas just writing 0 to a load of memory locations using MOV doesn't have this problem.

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

Reply by email

Sven Barth
to Florian Klämpfl: Inlining FillChar is for sure useful (same for move). The FillChar in the system unit…

Florian Klämpfl via fpc-devel schrieb am
Sa., 16. Apr. 2022, 21:00:

Show quoted text 131 lines
> Am 16.04.2022 um 01:26 schrieb J. Gareth Moreton via fpc-devel < >: > > Hi everyone, > > This is something that sprung to mind when thinking about code speed and the like, and one thing that cropped up is the initialisation of large variables such as arrays or records. A common means of doing this is, say: > > FillChar(MyVar, SizeOf(MyVar), 0); > > To keep things as general-purpose as possible, this usually results in a function call that decides the best course of action, and for very large blocks of data whose size may not be deterministic (e.g. a file buffer), this is the best approach - the overhead is relatively small and it quickly uses fast block-move instructions. > > However, for small-to-mid-sized variables of known size, this can lead to some inefficiencies, first by not taking into account that the size of the variable is known, but also because the initialisation value is zero, more often that not, and the variable is probably aligned on the stack (so the checks to make sure a pointer is aligned are unnecessary). > > I did a proof of concept on x86_64-win64 with the following record: > > type > TTestRecord = record > Field1: Byte; > Field2, Field3, Field4: Integer; > end; > > SizeOf(TTestRecord) is 16 and all the fields are on 4-byte boundaries. Nothing particularly special. > > I then declared a variable of this time and filled the fields with random values, and then ran two different methods to clear their memory. To get a good speed average, I ran each method 1,000,000,000 times in a for-loop. The first method was: > > FillChar(TestRecord, SizeOf(TestRecord), 0); > > The second method was inline assembly language (which I've called 'the intrinsic'): > > asm > PXOR XMM0, XMM0 > MOVDQU [RIP+TestRecord], XMM0 > end;2 > > It's not perfect because the presence of inline assembly prevents the use of register variables (although TestRecord is always on the stack regardless), but the performance hit is barely noticeable in this case, and if the assembly language were inserted by the compiler, the register variable problem won't arise. > > These are my results: > > FillChar time: 2.398 ns > > Field1 = 0 > Field2 = 0 > Field3 = 0 > Field4 = 0 > > Intrinsic time: 1.336 ns > > Field1 = 0 > Field2 = 0 > Field3 = 0 > Field4 = 0 > > Sure, it's on the order of nanoseconds, but the intrinsic is almost twice as fast. > > In terms of size - FillChar call = 20 bytes: > > 488d0d22080200 lea 0x20822(%rip),%rcx # 0x100022010 > 4531c0 xor %r8d,%r8d > ba10000000 mov $0x10,%edx > e8150a0000 callq 0x100002210 <SYSTEM_$$_FILLCHAR$formal$INT64$BYTE> > > The intrinsic = 12 bytes: > > 660fefc0 pxor %xmm0,%xmm0 > f30f7f05bd050200 movdqu %xmm0,0x205bd(%rip) # 0x100022010 > > For a 32-byte record instead, an extra 8-byte MOVDQU instruction would be required, so the 2 would be equal size, but with the bonus that the intrinsic doesn't have a function call and will probably help optimisation in the rest of the procedure by freeing up the registers used to pass parameters (%rcx, %rdx and %r8 in this case; although the intrinsic will require an MM register in this x86_64 example, they tend to not be used as often). Also, the peephole optimizer can remove redundant PXOR XMM0, XMM0 calls, which will help as well if there are multiple FillChar calls. > > I'm not proposing a total rewrite, and I would say that in the default case, it should just fall back to the in-built System functions, but the relevant compiler nodes could be overridden on specific platforms to generate smaller, more optimised code when the sizes and values are known at compile time. > > Now, in this example, it is still faster to simply set the fields manually one-by-one (clocks in at around 1.2 ns), possibly due to the unaligned write (MOVDQU) and internal SSE state switching adding some overhead, but there's nothing to stop the compiler from inserting code in place of the FillChar call to do just that if it thinks it's the fastest method. Then again, one has to be a little bit careful because FillChar and the intrinsic will also set the filler bytes between Field1 and Field2 to 0, whereas manually assigning 0 to the fields won't (so they aren't strictly equivalent and might only be allowed if there are no filler bytes or when compiling under -O4, but the latter may still be dangerous when typecasting is concerned), and extra care would have to be taken when unions are concerned (sorry, 'union' that's a C term - what's the official Pascal term again?). > > Actual Pascal calls to FillChar would not change in any way and so theoretically it won't break existing code. The only drawback is that the intrinsic and the internal System functions would have to be named the same so constructs such as "FuncPtr := @FillChar;" as well as calling FillChar from assembler routines stilll work, and the compiler would have to know how to differentiate between the two. > > Just on the surface, what are your thoughts? Inlining FillChar is for sure useful (same for move). The FillChar in the system unit could stay, the compiler could just replace a call to System.FillChar by some compiler generated assembler doing the FillChar.

But we should have a general mechanism for that, not something that just handles FillChar.

Regards,
Sven

Reply by email

Stefan Glienke
to J. Gareth Moreton: This is something that sprung to mind when thinking about code speed and the like, and…

If you want to zero small records more efficiently it might be better using Default(t) for that and looking at optimizing the code the compiler generates for that as it seems it produces an empty temp variable which it assigns instead of simply zeroing the record variable where default() is being assigned to.

Show quoted text 136 lines
On 16/04/2022 01:26 J. Gareth Moreton via fpc-devel wrote: Hi everyone, This is something that sprung to mind when thinking about code speed and the like, and one thing that cropped up is the initialisation of large variables such as arrays or records.  A common means of doing this is, say: FillChar(MyVar, SizeOf(MyVar), 0); To keep things as general-purpose as possible, this usually results in a function call that decides the best course of action, and for very large blocks of data whose size may not be deterministic (e.g. a file buffer), this is the best approach - the overhead is relatively small and it quickly uses fast block-move instructions. However, for small-to-mid-sized variables of known size, this can lead to some inefficiencies, first by not taking into account that the size of the variable is known, but also because the initialisation value is zero, more often that not, and the variable is probably aligned on the stack (so the checks to make sure a pointer is aligned are unnecessary). I did a proof of concept on x86_64-win64 with the following record: type   TTestRecord = record     Field1: Byte;     Field2, Field3, Field4: Integer;   end; SizeOf(TTestRecord) is 16 and all the fields are on 4-byte boundaries.  Nothing particularly special. I then declared a variable of this time and filled the fields with random values, and then ran two different methods to clear their memory.  To get a good speed average, I ran each method 1,000,000,000 times in a for-loop.  The first method was: FillChar(TestRecord, SizeOf(TestRecord), 0); The second method was inline assembly language (which I've called 'the intrinsic'): asm   PXOR   XMM0, XMM0   MOVDQU [RIP+TestRecord], XMM0 end;2 It's not perfect because the presence of inline assembly prevents the use of register variables (although TestRecord is always on the stack regardless), but the performance hit is barely noticeable in this case, and if the assembly language were inserted by the compiler, the register variable problem won't arise. These are my results:  FillChar time: 2.398 ns Field1 = 0 Field2 = 0 Field3 = 0 Field4 = 0 Intrinsic time: 1.336 ns Field1 = 0 Field2 = 0 Field3 = 0 Field4 = 0 Sure, it's on the order of nanoseconds, but the intrinsic is almost twice as fast. In terms of size - FillChar call = 20 bytes: 488d0d22080200           lea 0x20822(%rip),%rcx        # 0x100022010 4531c0                   xor    %r8d,%r8d ba10000000               mov    $0x10,%edx e8150a0000               callq  0x100002210 <SYSTEM_$$_FILLCHAR$formal$INT64$BYTE> The intrinsic = 12 bytes: 660fefc0                 pxor %xmm0,%xmm0 f30f7f05bd050200         movdqu %xmm0,0x205bd(%rip)        # 0x100022010 For a 32-byte record instead, an extra 8-byte MOVDQU instruction would be required, so the 2 would be equal size, but with the bonus that the intrinsic doesn't have a function call and will probably help optimisation in the rest of the procedure by freeing up the registers used to pass parameters (%rcx, %rdx and %r8 in this case; although the intrinsic will require an MM register in this x86_64 example, they tend to not be used as often).  Also, the peephole optimizer can remove redundant PXOR XMM0, XMM0 calls, which will help as well if there are multiple FillChar calls. I'm not proposing a total rewrite, and I would say that in the default case, it should just fall back to the in-built System functions, but the relevant compiler nodes could be overridden on specific platforms to generate smaller, more optimised code when the sizes and values are known at compile time. Now, in this example, it is still faster to simply set the fields manually one-by-one (clocks in at around 1.2 ns), possibly due to the unaligned write (MOVDQU) and internal SSE state switching adding some overhead, but there's nothing to stop the compiler from inserting code in place of the FillChar call to do just that if it thinks it's the fastest method.  Then again, one has to be a little bit careful because FillChar and the intrinsic will also set the filler bytes between Field1 and Field2 to 0, whereas manually assigning 0 to the fields won't (so they aren't strictly equivalent and might only be allowed if there are no filler bytes or when compiling under -O4, but the latter may still be dangerous when typecasting is concerned), and extra care would have to be taken when unions are concerned (sorry, 'union' that's a C term - what's the official Pascal term again?). Actual Pascal calls to FillChar would not change in any way and so theoretically it won't break existing code.  The only drawback is that the intrinsic and the internal System functions would have to be named the same so constructs such as "FuncPtr := @FillChar;" as well as calling FillChar from assembler routines stilll work, and the compiler would have to know how to differentiate between the two. Just on the surface, what are your thoughts? Garetha ka. Kit -- This email has been checked for viruses by Avast antivirus software. https://www.avast.com/antivirus _______________________________________________ fpc-devel maillist - https://lists.freepascal.org/cgi-bin/mailman/listinfo/fpc-devel

Reply by email

Sven Barth
to Stefan Glienke

Stefan Glienke via fpc-devel schrieb am
Di., 19. Apr. 2022, 12:38:

Show quoted text 6 lines
If you want to zero small records more efficiently it might be better using Default(t) for that and looking at optimizing the code the compiler generates for that as it seems it produces an empty temp variable which it assigns instead of simply zeroing the record variable where default() is being assigned to.

This was an explicit design choice I made, because it pays of as soon as a second such assignment for the same type is made.

Regards,
Sven

Reply by email

Stefan Glienke
to Sven Barth

You are the expert but I am not sure how that can be the case given you only need to zero a register and blast that into the record location opposed to twice as many mov operations being generated that I have seen with the record that Gareth originally posted.

Show quoted text 14 lines
On 19/04/2022 13:37 Sven Barth via fpc-devel wrote: Stefan Glienke via fpc-devel schrieb am Di., 19. Apr. 2022, 12:38: > If you want to zero small records more efficiently it might be better using Default(t) for that and looking at optimizing the code the compiler generates for that as it seems it produces an empty temp variable which it assigns instead of simply zeroing the record variable where default() is being assigned to. This was an explicit design choice I made, because it pays of as soon as a second such assignment for the same type is made. Regards, Sven > _______________________________________________ fpc-devel maillist - https://lists.freepascal.org/cgi-bin/mailman/listinfo/fpc-devel

Reply by email

J. Gareth Moreton
to Stefan Glienke

Interesting - I wasn't aware of this intrinsic!  I'll make a note of that one.

It might be useful to transform FillChar calls to the Default intrinsic at the node level.

Gareth aka. Kit

Stefan Glienke wrote 18 lines
You are the expert but I am not sure how that can be the case given you only need to zero a register and blast that into the record location opposed to twice as many mov operations being generated that I have seen with the record that Gareth originally posted. > On 19/04/2022 13:37 Sven Barth via fpc-devel wrote: > > > Stefan Glienke via fpc-devel schrieb am Di., 19. Apr. 2022, 12:38: >> If you want to zero small records more efficiently it might be better using Default(t) for that and looking at optimizing the code the compiler generates for that as it seems it produces an empty temp variable which it assigns instead of simply zeroing the record variable where default() is being assigned to. > This was an explicit design choice I made, because it pays of as soon as a second such assignment for the same type is made. > > Regards, > Sven > _______________________________________________ > fpc-devel maillist - > https://lists.freepascal.org/cgi-bin/mailman/listinfo/fpc-devel _______________________________________________ fpc-devel maillist - https://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

Reply by email

Stefan Glienke
to J. Gareth Moreton

I would rather give a hint to change the code - because only for unmanaged types the semantic of FillChar is equal to default().

As for performance - I just ran some benchmarks comparing the way FPC does it and how Delphi does it. xor a register and mov that into place (Delphi way) is faster in all cases I tested. Also once the record is large enough FPC uses rep movs, Delphi does rep stos. rep stos has faster timings that rep movs so I cannot see how it could be faster given the initial overhead to produce the empty temp.

Show quoted text 38 lines
On 19/04/2022 14:49 J. Gareth Moreton via fpc-devel wrote: Interesting - I wasn't aware of this intrinsic!  I'll make a note of that one. It might be useful to transform FillChar calls to the Default intrinsic at the node level. Gareth aka. Kit On 19/04/2022 12:43, Stefan Glienke via fpc-devel wrote: > You are the expert but I am not sure how that can be the case given you only need to zero a register and blast that into the record location opposed to twice as many mov operations being generated that I have seen with the record that Gareth originally posted. > >> On 19/04/2022 13:37 Sven Barth via fpc-devel wrote: >> >> >> Stefan Glienke via fpc-devel schrieb am Di., 19. Apr. 2022, 12:38: >>> If you want to zero small records more efficiently it might be better using Default(t) for that and looking at optimizing the code the compiler generates for that as it seems it produces an empty temp variable which it assigns instead of simply zeroing the record variable where default() is being assigned to. >> This was an explicit design choice I made, because it pays of as soon as a second such assignment for the same type is made. >> >> Regards, >> Sven >> _______________________________________________ >> fpc-devel maillist - >> https://lists.freepascal.org/cgi-bin/mailman/listinfo/fpc-devel > _______________________________________________ > fpc-devel maillist - > https://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 - https://lists.freepascal.org/cgi-bin/mailman/listinfo/fpc-devel

Reply by email

J. Gareth Moreton
to Stefan Glienke

At least for me, I think most programmers know not to use FillChar to fill a managed type, but it is a good point to be careful.

Zeroing a register and moving it into a block is usually the fastest way to go about such things, but FillChar doesn't assume the input is zero, whereas if it were treated as an intrinsic, such an optimisation can be made.

I'm not sure why Free Pascal would use REP MOVS to fill a buffer, since that's for moving data from one buffer to another of the same size (e.g. what Move sometimes uses), whereas REP STOS writes the value in AL/AX/EAX/RAX to a destination buffer, so of course REP MOVS is going to be slower.

Gareth aka. Kit

Stefan Glienke wrote 45 lines
I would rather give a hint to change the code - because only for unmanaged types the semantic of FillChar is equal to default(). As for performance - I just ran some benchmarks comparing the way FPC does it and how Delphi does it. xor a register and mov that into place (Delphi way) is faster in all cases I tested. Also once the record is large enough FPC uses rep movs, Delphi does rep stos. rep stos has faster timings that rep movs so I cannot see how it could be faster given the initial overhead to produce the empty temp. > On 19/04/2022 14:49 J. Gareth Moreton via fpc-devel wrote: > > > Interesting - I wasn't aware of this intrinsic!  I'll make a note of > that one. > > It might be useful to transform FillChar calls to the Default intrinsic > at the node level. > > Gareth aka. Kit > > On 19/04/2022 12:43, Stefan Glienke via fpc-devel wrote: >> You are the expert but I am not sure how that can be the case given you only need to zero a register and blast that into the record location opposed to twice as many mov operations being generated that I have seen with the record that Gareth originally posted. >> >>> On 19/04/2022 13:37 Sven Barth via fpc-devel wrote: >>> >>> >>> Stefan Glienke via fpc-devel schrieb am Di., 19. Apr. 2022, 12:38: >>>> If you want to zero small records more efficiently it might be better using Default(t) for that and looking at optimizing the code the compiler generates for that as it seems it produces an empty temp variable which it assigns instead of simply zeroing the record variable where default() is being assigned to. >>> This was an explicit design choice I made, because it pays of as soon as a second such assignment for the same type is made. >>> >>> Regards, >>> Sven >>> _______________________________________________ >>> fpc-devel maillist - >>> https://lists.freepascal.org/cgi-bin/mailman/listinfo/fpc-devel >> _______________________________________________ >> fpc-devel maillist - >> https://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 - > https://lists.freepascal.org/cgi-bin/mailman/listinfo/fpc-devel _______________________________________________ fpc-devel maillist - https://lists.freepascal.org/cgi-bin/mailman/listinfo/fpc-devel

Reply by email