Sign in Join

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

Mailing lists / fpc-devel / 2020-01

The new XMM intrinsics

13 messages · 3 people · –

Reply by email Subscribe to this list Your subscription

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

J. Gareth Moreton

Hey everyone,

Maybe I'm being a bit pedantic with this, but must we abide by C/C++ standards and go by the name __m128 etc. for the 128-bit data type?  Being as how Pascal tended to go for more readable and BASIC-inspired names like Integer and Single, might it be better to name them TM128 instead?  If not that, then is it possible to add a union-like record type to the System unit or the inc files that contain all of the intrinsics?

My vectorcall tests (e.g. tests\test\cg\tvectorcall1.pp) have something like this:

{$PUSH}
{$CODEALIGN RECORDMIN=16}
{$PACKRECORDS C}
type
   TM128 = record
     case Byte of
       0: (M128_F32: array[0..3] of Single);
       1: (M128_F64: array[0..1] of Double);
   end;
{$POP}

Granted, given that __m128 will be automatically aligned, all of the codealign directives may not be necessary - for example:

type
   TM128 = record
     case Byte of
       0: (M128_F32: array[0..3] of Single);
       2: (M128_F64: array[0..1] of Double);
       3: (M128_Internal: __m128);
   end;

The main thing I'm thinking about is that it's actually rather difficult to modify the elements of a variable of type __m128 directly in C/C++ because of the type being opaque and difficult to typecast sometimes (some compilers will treat it as an array, others will treat it as a record type like the above (Visual C++ does this), while others may not allow access to its elements at all).  Often, I might want to map a 4-component vector with Single-type fields x, y, z and w to an aligned __m128 type, or Double-type fields Re and Im when dealing with complex numbers. That way, I can read from and write to them outside of intrinsic calls.

I suppose I'm suggesting we introduce something more usable than what C has so people can actually use intrinsics more easily.

Gareth aka. Kit

Reply by email

J. Gareth Moreton
to J. Gareth Moreton

As a minor additional note, I think some of the mm types are declared incorrectly in compiler/psystem.pas - e.g. line 411:

tarraydef(x86_m128itype).elementdef:=s32floattype;

Shouldn't that be s32inttype or some other Integer type?  m128i is for when you're storing integers in the XMM registers.

Gareth aka. Kit

J. Gareth Moreton wrote 55 lines
Hey everyone, Maybe I'm being a bit pedantic with this, but must we abide by C/C++ standards and go by the name __m128 etc. for the 128-bit data type?  Being as how Pascal tended to go for more readable and BASIC-inspired names like Integer and Single, might it be better to name them TM128 instead?  If not that, then is it possible to add a union-like record type to the System unit or the inc files that contain all of the intrinsics? My vectorcall tests (e.g. tests\test\cg\tvectorcall1.pp) have something like this: {$PUSH} {$CODEALIGN RECORDMIN=16} {$PACKRECORDS C} type   TM128 = record     case Byte of       0: (M128_F32: array[0..3] of Single);       1: (M128_F64: array[0..1] of Double);   end; {$POP} Granted, given that __m128 will be automatically aligned, all of the codealign directives may not be necessary - for example: type   TM128 = record     case Byte of       0: (M128_F32: array[0..3] of Single);       2: (M128_F64: array[0..1] of Double);       3: (M128_Internal: __m128);   end; The main thing I'm thinking about is that it's actually rather difficult to modify the elements of a variable of type __m128 directly in C/C++ because of the type being opaque and difficult to typecast sometimes (some compilers will treat it as an array, others will treat it as a record type like the above (Visual C++ does this), while others may not allow access to its elements at all).  Often, I might want to map a 4-component vector with Single-type fields x, y, z and w to an aligned __m128 type, or Double-type fields Re and Im when dealing with complex numbers. That way, I can read from and write to them outside of intrinsic calls. I suppose I'm suggesting we introduce something more usable than what C has so people can actually use intrinsics more easily. Gareth aka. Kit _______________________________________________ fpc-devel maillist  -  https://lists.freepascal.org/cgi-bin/mailman/listinfo/fpc-devel

Reply by email

Florian Klämpfl
to J. Gareth Moreton: Maybe I'm being a bit pedantic with this, but must we abide by C/C++ standards and go by…
J. Gareth Moreton wrote
Hey everyone, Maybe I'm being a bit pedantic with this, but must we abide by C/C++ standards and go by the name __m128 etc. for the 128-bit data type?

They can be added additionally. After some thinking, I came to the conclusion we should first go with the same naming C uses (as we do in every library header meanwhile), on top of this some more pascalish way can be added.

Show quoted text 5 lines
Being as how Pascal tended to go for more readable and BASIC-inspired names like Integer and Single, might it be better to name them TM128 instead?  If not that, then is it possible to add a union-like record type to the System unit or the inc files that contain all of the intrinsics?

The point is: only these types enable the compiler to use the right mov instructions. Using a record means there is no rule which mov instructions (movdqa, movabs, movapd etc.) should be used.

Show quoted text 45 lines
My vectorcall tests (e.g. tests\test\cg\tvectorcall1.pp) have something like this: {$PUSH} {$CODEALIGN RECORDMIN=16} {$PACKRECORDS C} type   TM128 = record     case Byte of       0: (M128_F32: array[0..3] of Single);       1: (M128_F64: array[0..1] of Double);   end; {$POP} Granted, given that __m128 will be automatically aligned, all of the codealign directives may not be necessary - for example: type   TM128 = record     case Byte of       0: (M128_F32: array[0..3] of Single);       2: (M128_F64: array[0..1] of Double);       3: (M128_Internal: __m128);   end; The main thing I'm thinking about is that it's actually rather difficult to modify the elements of a variable of type __m128 directly in C/C++ because of the type being opaque and difficult to typecast sometimes (some compilers will treat it as an array, others will treat it as a record type like the above (Visual C++ does this), while others may not allow access to its elements at all).  Often, I might want to map a 4-component vector with Single-type fields x, y, z and w to an aligned __m128 type, or Double-type fields Re and Im when dealing with complex numbers. That way, I can read from and write to them outside of intrinsic calls. I suppose I'm suggesting we introduce something more usable than what C has so people can actually use intrinsics more easily. Gareth aka. Kit _______________________________________________ fpc-devel maillist  -  https://lists.freepascal.org/cgi-bin/mailman/listinfo/fpc-devel

Reply by email

Sven Barth
to J. Gareth Moreton: Maybe I'm being a bit pedantic with this, but must we abide by C/C++ standards and go by…
J. Gareth Moreton wrote 9 lines
Hey everyone, Maybe I'm being a bit pedantic with this, but must we abide by C/C++ standards and go by the name __m128 etc. for the 128-bit data type?  Being as how Pascal tended to go for more readable and BASIC-inspired names like Integer and Single, might it be better to name them TM128 instead?  If not that, then is it possible to add a union-like record type to the System unit or the inc files that contain all of the intrinsics?

I agree that the names with __xxx for the SIMD types is a bad choice. In C/C++ they did this to avoid type conflicts (after all types with two underscores are "reserved"), but in Pascal we don't have this problem as the System types will be hidden by other units that declare similar types, but can still be used by using System.TheType.

Thus I personally would prefer more Pascal-style names for these as well (though I don't think that TXXX is good, because no other primitive type starts with a T and that's what those types essentially are: primitive, base types). So maybe simply M128 instead of __m128 would be better (and analogous for the other types). This would be similar to the "new" integer aliases: UInt8, Int8, Int32, UInt32, etc.

Show quoted text 39 lines
My vectorcall tests (e.g. tests\test\cg\tvectorcall1.pp) have something like this: {$PUSH} {$CODEALIGN RECORDMIN=16} {$PACKRECORDS C} type   TM128 = record     case Byte of       0: (M128_F32: array[0..3] of Single);       1: (M128_F64: array[0..1] of Double);   end; {$POP} Granted, given that __m128 will be automatically aligned, all of the codealign directives may not be necessary - for example: type   TM128 = record     case Byte of       0: (M128_F32: array[0..3] of Single);       2: (M128_F64: array[0..1] of Double);       3: (M128_Internal: __m128);   end; The main thing I'm thinking about is that it's actually rather difficult to modify the elements of a variable of type __m128 directly in C/C++ because of the type being opaque and difficult to typecast sometimes (some compilers will treat it as an array, others will treat it as a record type like the above (Visual C++ does this), while others may not allow access to its elements at all).  Often, I might want to map a 4-component vector with Single-type fields x, y, z and w to an aligned __m128 type, or Double-type fields Re and Im when dealing with complex numbers. That way, I can read from and write to them outside of intrinsic calls. I suppose I'm suggesting we introduce something more usable than what C has so people can actually use intrinsics more easily.

I don't know the plans of Florian, but I would very well imagine that code like the following is going to be valid:

=== code begin ===

type
   i: array[0..3] of LongInt;
   m: __m128i;
begin
   m := i;
   // or
   i := m;
end.

=== code end ===

With that working and type helpers one can implement the following:

=== code begin ===

type
   TM128Helper = type helper for __m128
   public type
     TLongIntIndex = 0..3;
   private type
     TLongIntArray = array[TLongIntIndex] of LongInt;
   private
     procedure SetAsLongInt(aIndex: TLongIntIndex; aValue: LongInt); inline;
     function GetAsLongInt(aIndex: TLongIntIndex): LongInt; inline;
   public
     property AsLongInt[Index: TLongIntIndex]: LongInt read GetAsLongInt
write SetAsLongInt;
   end;
//
procedure TM128Helper.SetAsLongInt(aIndex: TLongIntIndex; aValue: LongInt);
begin
   TLongIntArray(Self)[aIndex] := aValue;
end;
function TM128Helper.GetAsLongInt(aIndex: TLongIntIndex): LongInt;
begin
   Result := TLongIntArray(Self)[aIndex];
end;

=== code end ===

This would allow to move those conversions from being handled by some compiler magic to the runtime library.

In fact quite a bit of it is already working now, though the generated assembly is not yet optimal (but the feature is still work in progress after all):

=== code begin ===

program tmmtest;
{$mode objfpc}
{$modeswitch typehelpers}
type
   TM128Helper = type helper for __m128
   public type
     TLongIntIndex = 0..3;
   private type
     TLongIntArray = array[0..3] of LongInt;
   private
    procedure SetAsLongInt(aIndex: TLongIntIndex; aValue: LongInt);
inline; vectorcall;
    function GetAsLongInt(aIndex: TLongIntIndex): LongInt; inline;
vectorcall;
  public
    property AsLongInt[Index: TLongIntIndex]: LongInt read GetAsLongInt
write SetAsLongInt;
  end;
procedure TM128Helper.SetAsLongInt(aIndex: TLongIntIndex; aValue:
LongInt); vectorcall;
var
   arr: TLongIntArray;
begin
   x86_movups(@arr[0], Self);
   arr[aIndex] := aValue;
   // triggers internal error 200310081
   //Self := x86_movups(@arr[0]);
end;
function TM128Helper.GetAsLongInt(aIndex: TLongIntIndex): LongInt;
vectorcall;
var
   arr: TLongIntArray;
begin
   x86_movups(@arr[0], Self);
   Result := arr[aIndex];
end;
procedure Test;
var
   m: __m128;
   i: LongInt;
begin
   m.AsLongInt[0] := 42;
   i := m.AsLongInt[0];
end;
begin
   Test;
end.

=== code end ===

The generated assembly for Test is this:

=== code begin ===

# Var m located at rbp-16, size=OS_M128
# Var i located at rbp-20, size=OS_S32
# [42] m.AsLongInt[0] := 42;
    leaq    -36(%rbp),%rax
    movdqa    -16(%rbp),%xmm0
    movups    %xmm0,(%rax)
    movl    $42,-36(%rbp)
# [43] i := m.AsLongInt[0];
    leaq    -36(%rbp),%rax
    movdqa    -16(%rbp),%xmm0
    movups    %xmm0,(%rax)
    movl    -36(%rbp),%eax
    movl    %eax,-20(%rbp)
# [44] end;

=== code end ===

Regards,
Sven

Reply by email

Sven Barth
to Florian Klämpfl: They can be added additionally. After some thinking, I came to the conclusion we should…
Florian Klämpfl wrote 10 lines
Am 16.01.20 um 23:22 schrieb J. Gareth Moreton: > Hey everyone, > > Maybe I'm being a bit pedantic with this, but must we abide by C/C++ > standards and go by the name __m128 etc. for the 128-bit data type? They can be added additionally. After some thinking, I came to the conclusion we should first go with the same naming C uses (as we do in every library header meanwhile), on top of this some more pascalish way can be added.

But these are not library headers. These are going to be base language types and I don't see a real reason to follow the C convention here, only because they were the first and other developers might easily recognize them this way. As mentioned in my other mail we don't have the name conflict problem that C/C++ faced when adding these types.

Regards,
Sven

Reply by email

Florian Klämpfl
to Sven Barth
Sven Barth wrote 16 lines
Am 18.01.2020 um 12:46 schrieb Florian Klämpfl: > Am 16.01.20 um 23:22 schrieb J. Gareth Moreton: >> Hey everyone, >> >> Maybe I'm being a bit pedantic with this, but must we abide by C/C++ >> standards and go by the name __m128 etc. for the 128-bit data type? > > They can be added additionally. After some thinking, I came to the > conclusion we should first go with the same naming C uses (as we do in > every library header meanwhile), on top of this some more pascalish > way can be added. But these are not library headers. These are going to be base language types and I don't see a real reason to follow the C convention here, only because they were the first and other developers might easily recognize them this way. As mentioned in my other mail we don't have the name conflict problem that C/C++ faced when adding these types.

* there is a lot of example code out there for C, and same naming makes it much easier to use it in FPC. Actually, I encountered this already when playing with some C examples from SO. Even though I am very familiar with this topic, I ended up to put the C code into godbolt to find out which assembler instructions it uses and then I tried to find the right FPC intrinsics (we don't :(). Lesson learned :)
* finding a systematic, pascal-like naming scheme is not easy, there are countless cases to consider
* they are part of the system unit, so they are prone to name clashes (this is something I were thinking to try: if they could be moved without too much hazzle into a seperate unit)

Reply by email

Sven Barth
to Florian Klämpfl
Florian Klämpfl wrote 26 lines
Am 18.01.20 um 12:53 schrieb Sven Barth via fpc-devel: > Am 18.01.2020 um 12:46 schrieb Florian Klämpfl: >> Am 16.01.20 um 23:22 schrieb J. Gareth Moreton: >>> Hey everyone, >>> >>> Maybe I'm being a bit pedantic with this, but must we abide by >>> C/C++ standards and go by the name __m128 etc. for the 128-bit data >>> type? >> >> They can be added additionally. After some thinking, I came to the >> conclusion we should first go with the same naming C uses (as we do >> in every library header meanwhile), on top of this some more >> pascalish way can be added. > But these are not library headers. These are going to be base > language types and I don't see a real reason to follow the C > convention here, only because they were the first and other > developers might easily recognize them this way. As mentioned in my > other mail we don't have the name conflict problem that C/C++ faced > when adding these types. * there is a lot of example code out there for C, and same naming makes it much easier to use it in FPC. Actually, I encountered this already when playing with some C examples from SO. Even though I am very familiar with this topic, I ended up to put the C code into godbolt to find out which assembler instructions it uses and then I tried to find the right FPC intrinsics (we don't :(). Lesson learned :)

I don't necessarily mean the intrinsics (though we could still declare them as e.g. x86_mm_extract_epi32, so that it's clear that A) it's x86 only and B) that they have the same name part as the C examples). But the type names should be more Pascal like. For day to day use I imagine most of the intrinsics to be hidden by normal Pascal language code (e.g. somearray := somessevar calling the corresponding intrinsic). However the types will be used more (also we can document that these are equivalent to the corresponding C types). And for those that want C compatible type names we can always add aliases to the ctypes unit.

Show quoted text 5 lines
* finding a systematic, pascal-like naming scheme is not easy, there are countless cases to consider * they are part of the system unit, so they are prone to name clashes (this is something I were thinking to try: if they could be moved without too much hazzle into a seperate unit)

But those name clashes are restricted to the System unit which, while not that small, is still finite. The C/C++ devs had to consider all code out there due to the missing concept of namespaces.

Regards,
Sven

Reply by email

J. Gareth Moreton
to Sven Barth

I think more than anything I would like to see a degree of "ease of use" for the SIMD intrinsics, since modifying __m128 data in C/C++ is not that straightforward.  Just as a straightforward example, what might be the best way to ensure these following record types are compatible with the __m128 data types?

*type *TVector4 = *record*
   X, Y, Z, W: Single;
*end*; // compatible with __m128f
*type *TComplex = *record*
   Re, Im: Double;
*end*; // compatible with __m128d

Programmers on their own projects, whether it be a game or a mathematical program, would much rather work with those constructs if it can be helped... or at least I would.

Gareth aka. Kit

Reply by email

Florian Klämpfl
to J. Gareth Moreton
J. Gareth Moreton wrote
I think more than anything I would like to see a degree of "ease of use" for the SIMD intrinsics, since modifying __m128 data in C/C++ is not that straightforward.

... because there exist in general no instructions to do so, e.g. you cannot access a xmm register indexed with sse. Those intrinsics provide only what the CPU can do directly.

Show quoted text 15 lines
Just as a straightforward example, what might be the best way to ensure these following record types are compatible with the __m128 data types? *type *TVector4 = *record*   X, Y, Z, W: Single; *end*; // compatible with __m128f *type *TComplex = *record*   Re, Im: Double; *end*; // compatible with __m128d Programmers on their own projects, whether it be a game or a mathematical program, would much rather work with those constructs if it can be helped... or at least I would.

This is another level of abstraction, the intrinsics aim really at a direct usage of the sse/avx instructions. No more, no less.

Reply by email

J. Gareth Moreton
to Florian Klämpfl

I personally would have thought the compiler would be smart enough to promote a 128-bit array to an XMM register when among intrinstics or just passing the structure as a whole from one place to another, but utilise memory movement or the scalar SSE commands when modifying individual fields.  I guess it's easier said than done though and probably something for me to research later on.

At the very least though, there should be a way to cleanly align a structure to a 16-byte granularity in memory, since many of the SSE instructions require this.  I don't consider pre-processor directives to be particularly clean and easy to use.  True, one can use the "Move" instruction to move data from your customised structure to an __m128, and back again when you're finished, but this feels like an unnecessary degree of overhead.

Gareth aka. Kit

Florian Klämpfl wrote 31 lines
Am 18.01.20 um 18:52 schrieb J. Gareth Moreton: > I think more than anything I would like to see a degree of "ease of > use" for the SIMD intrinsics, since modifying __m128 data in C/C++ is > not that straightforward. ... because there exist in general no instructions to do so, e.g. you cannot access a xmm register indexed with sse. Those intrinsics provide only what the CPU can do directly. > Just as a straightforward example, what might be the best way to > ensure these following record types are compatible with the __m128 > data types? > > *type *TVector4 = *record* >    X, Y, Z, W: Single; > *end*; // compatible with __m128f > > *type *TComplex = *record* >    Re, Im: Double; > *end*; // compatible with __m128d > > Programmers on their own projects, whether it be a game or a > mathematical program, would much rather work with those constructs if > it can be helped... or at least I would. This is another level of abstraction, the intrinsics aim really at a direct usage of the sse/avx instructions. No more, no less. _______________________________________________ fpc-devel maillist  -  https://lists.freepascal.org/cgi-bin/mailman/listinfo/fpc-devel

Reply by email

Florian Klämpfl
to J. Gareth Moreton
J. Gareth Moreton wrote 6 lines
I personally would have thought the compiler would be smart enough to promote a 128-bit array to an XMM register when among intrinstics or just passing the structure as a whole from one place to another, but utilise memory movement or the scalar SSE commands when modifying individual fields.  I guess it's easier said than done though and probably something for me to research later on.

As said: this is not the purpose of those intrinsics. They can be used to do so though but this is another story.

Reply by email

Sven Barth
to J. Gareth Moreton: I think more than anything I would like to see a degree of "ease of use" for the SIMD…
J. Gareth Moreton wrote 19 lines
I think more than anything I would like to see a degree of "ease of use" for the SIMD intrinsics, since modifying __m128 data in C/C++ is not that straightforward.  Just as a straightforward example, what might be the best way to ensure these following record types are compatible with the __m128 data types? *type *TVector4 = *record*   X, Y, Z, W: Single; *end*; // compatible with __m128f *type *TComplex = *record*   Re, Im: Double; *end*; // compatible with __m128d Programmers on their own projects, whether it be a game or a mathematical program, would much rather work with those constructs if it can be helped... or at least I would.

The compiler currently considers corresponding arrays as compatible, like "array[0..3] of Single" or "array[0..1] of Double". In theory nothing would stop us from implementing similar compatibility with the records you mentioned.

Though to be really useable it would need to be flexible enough to handle for the following for example as well:

=== code begin ===

type
   TVector3 = record
     X, Y, Z: Single;
   end;
   TVector4 = record
     P: TVector3;
     W: Single;
   end;

=== code end ===

Regards,
Sven

Reply by email

J. Gareth Moreton
to Sven Barth

Seems fair.

The main thing that I'm wary of is memory alignment.  SSE works best with structures that are aligned to a 16-byte boundary - attempting to call MOVAPS with a structure that isn't aligned will trigger an access violation, while MOVUPS is considerably slower on older processors.  This is why I suggested something like the following some time ago, instead of depending on slightly messy pre-processor directives:

*type *TVector4 = *record*
  X, Y, Z, W: Single
*end **align *16;

Gareth aka. Kit

Sven Barth wrote 48 lines
Am 18.01.2020 um 18:52 schrieb J. Gareth Moreton: > > I think more than anything I would like to see a degree of "ease of > use" for the SIMD intrinsics, since modifying __m128 data in C/C++ is > not that straightforward.  Just as a straightforward example, what > might be the best way to ensure these following record types are > compatible with the __m128 data types? > > *type *TVector4 = *record* >   X, Y, Z, W: Single; > *end*; // compatible with __m128f > > *type *TComplex = *record* >   Re, Im: Double; > *end*; // compatible with __m128d > > Programmers on their own projects, whether it be a game or a > mathematical program, would much rather work with those constructs if > it can be helped... or at least I would. > The compiler currently considers corresponding arrays as compatible, like "array[0..3] of Single" or "array[0..1] of Double". In theory nothing would stop us from implementing similar compatibility with the records you mentioned. Though to be really useable it would need to be flexible enough to handle for the following for example as well: === code begin === type   TVector3 = record     X, Y, Z: Single;   end;   TVector4 = record     P: TVector3;     W: Single;   end; === code end === Regards, Sven _______________________________________________ fpc-devel maillist - https://lists.freepascal.org/cgi-bin/mailman/listinfo/fpc-devel

Reply by email