https://gcc.gnu.org/bugzilla/show_bug.cgi?id=97222

            Bug ID: 97222
           Summary: GCC discards attributes aligned and may_alias for
                    typedefs passed as template arguments
           Product: gcc
           Version: 10.2.0
            Status: UNCONFIRMED
          Severity: normal
          Priority: P3
         Component: c++
          Assignee: unassigned at gcc dot gnu.org
          Reporter: mte.zych at gmail dot com
  Target Milestone: ---

Hello!

GCC discards aligned attribute, applied on a typdef, when it's passed as a
template argument.

Compiler Expolorer:
 GCC -> https://godbolt.org/z/bj8v1T

C++ Source Code:
 #include <iostream>

 typedef float vec __attribute__((vector_size(8)));
 typedef float fp  __attribute__((aligned(16)));

 template <typename t> struct identity { typedef t type; };

 int main ()
 {
     std::cout << sizeof(typename identity<vec>::type) << std::endl;
     std::cout << sizeof(vec                         ) << std::endl;

     std::cout << alignof(typename identity<fp>::type) << std::endl;
     std::cout << alignof(fp                         ) << std::endl;
 }

Program Output:
 8
 8
 4
 16

Compilation Log:
 warning: ignoring attributes on template argument 'fp' {aka 'float'}
[-Wignored-attributes]
 13 |     std::cout << alignof(typename identity<fp>::type) << std::endl;
    |                                              ^

The above program shows that alignment of the fp typedef changes, after it's
been
passed through the identity meta-function - it's 4-bytes instead of expected
16-bytes.


What's interesting is not all type attributes are discarded - the type
attribute vector_size
is preserved after being passed through the identity meta-function.

This behavior is required, since removal of the vector_size attribute
would be a semantic change of the vec type, affecting even its size,
because the vec type would represent a single float, instead of a vector of 2
floats.

The same could be said about the aligned attribute - discarding it is also a
semantic change,
since alignment is a fundamental property of a type, affecting among others
code generation,
that is, two types are not equivalent if they have different alignment.

This is the reason why I argue that, passing a typedef as a template argument
should preserve its aligned attribute, instead of discarding it.


Moreover, the Intel C++ compiler implements this behavior correctly.

Compiler Expolorer:
 ICC -> https://godbolt.org/z/9vr9se

Program Output:
 8
 8
 16
 16


The issue described above doesn't apply only to the aligned type attribute,
but also to the may_alias type attribute.

Compiler Expolorer:
 GCC -> https://godbolt.org/z/6EqsnP

C++ Source Code:
 typedef int integer __attribute__((may_alias));

 template <typename t> struct identity { typedef t type; };

 int main ()
 {
     typename identity<integer>::type i;
 }

Compilation Log:
 warning: ignoring attributes on template argument 'integer' {aka 'int'}
[-Wignored-attributes]
     7 |     typename identity<integer>::type i;
       |                              ^

Again, discarding attribute may_alias is a semantic change,
because aliasing rules can affect code generation.


Why this issue is important?
Well, because it prevents generic programming, via C++ templates, using x86
SIMD types.

If we would look at definitions of x86 SIMD types, we will notice that
they are essentially typedefs with attributes vector_size and may_alias applied
on them:

- immintrin.h
    typedef float     __m256  __attribute__((__vector_size__(32),
__may_alias__));

- emmintrin.h
    typedef long long __m128i __attribute__((__vector_size__(16),
__may_alias__));
    typedef double    __m128d __attribute__((__vector_size__(16),
__may_alias__));

- xmmintrin.h
    typedef float     __m128  __attribute__((__vector_size__(16),
__may_alias__)); 

Note that, the may_alias attributes is required and cannot be removed:

- /usr/lib/gcc/x86_64-linux-gnu/10/include/immintrin.h
    /* The Intel API is flexible enough that we must allow aliasing with other
       vector types, and their scalar components.  */

Compiler Expolorer:
 GCC -> https://godbolt.org/z/vz4fWK

C++ Source Code:
 #include <immintrin.h>

 template <typename t> struct identity { typedef t type; };

 int main ()
 {
     typename identity<__m128>::type fvec4;
 }

Compilation Log:
 warning: ignoring attributes on template argument '__m128'
[-Wignored-attributes]
     8 |     typename identity<__m128>::type fvec4;
       |                             ^


What's the root cause of this problem?

Well, the problem is in C++ a typedef is just an alias (a new name) for the old
type,
that is, it does *not* introduce a new type.

Implementing support for attributes vector_size, aligned and may_alias
in C++ typedefs requires an opaque/strong typedef,
introducing a brand new type and storing information about applied attributes.

 typedef float fp __attribute__((aligned(16)));

Think about it - a typedef introducing the fp type has to create a new type,
because even though both fp and float types represent floating point numbers
identically,
the fp type is *not* the float type, because these types have different
alignment requirements.


Note that, the Intel C++ Compiler does not introduce new types for typedefs,
which have attributes aligned or may_alias applied on them.

Compiler Explorer:
 ICC -> https://godbolt.org/z/MjdMqx

C++ Source Code:
 #include <iostream>

 typedef int vectorized_int __attribute__((vector_size(8)));
 typedef int    aligned_int __attribute__((aligned(16)));
 typedef int   aliasing_int __attribute__((may_alias));

 int main ()
 {
     std::cout << typeid(           int).name() << std::endl;
     std::cout << typeid(vectorized_int).name() << std::endl;
     std::cout << typeid(   aligned_int).name() << std::endl;
     std::cout << typeid(  aliasing_int).name() << std::endl;
 }

Program Output:
 i
 Dv2_i
 i
 i


However, this behavior leads to a contradiction,
in which there can exists a single type, which has 2 different alignment
requirements.

Compiler Explorer:
 ICC -> https://godbolt.org/z/4o9o3M

C++ Source Code:
 template <class, class> struct is_same        { static const auto value =
false; };
 template <class T>      struct is_same <T, T> { static const auto value = 
true; };

 typedef float fp __attribute__((aligned(16)));

 template <typename first_type, typename second_type>
 struct check_same
 {
     static_assert(is_same<first_type, second_type>::value    , "");
     static_assert( sizeof(first_type) ==  sizeof(second_type), "");
     static_assert(alignof(first_type) == alignof(second_type), "");
 };

 int main ()
 {
     check_same<int, signed int> { };
     check_same< fp,      float> { };
 }

Compilation Log:
 error: static assertion failed
     static_assert(alignof(first_type) == alignof(second_type), "");
     ^
 detected during instantiation of class
 "check_same<first_type, second_type> [with first_type=fp={float},
second_type=float]"

To avoid these kind of issues, GCC could replicate the behavior of the
vector_size attribute,
that is, introduce a brand new type and store information about applied
attributes.


Thank you, Mateusz Zych

PS. I want to thank Ivo Kabadshow from JSC for helping me with preparing these
code samples!

Reply via email to