Search code examples
c++visual-c++armintrinsicsneon

Error C2078 when initializing uint32x4_t on ARM?


I'm testing an ARM build using Visual Studio 2013. I'm catching a compile error when initializing a uint32x4_t. The error is error C2078: too many initializers.

const uint32x4_t CTRS[3] = {
    {1,0,0,0}, {2,0,0,0}, {3,0,0,0}
};

It results in:

cl.exe /nologo /W4 /wd4231 /wd4511 /wd4156 /D_MBCS /Zi /TP /GR /EHsc /DNDEBUG /D_
NDEBUG /Oi /Oy /O2 /MT /FI sdkddkver.h /FI winapifamily.h /DWINAPI_FAMILY=WINAPI_
FAMILY_PHONE_APP /c chacha_simd.cpp
chacha_simd.cpp
chacha_simd.cpp(306) : error C2078: too many initializers
NMAKE : fatal error U1077: '"C:\Program Files (x86)\Microsoft Visual Studio 12.0
\VC\BIN\x86_ARM\cl.exe"' : return code '0x2'
Stop.

I see it is a known issue from MSDN forums "error C2078: too many initializers" when using ARM NEON. It was acknowledged but no workaround was provided.

I also tried this awfulness (borrowing from PowerPC style):

const uint32x4_t CTRS[3] = {
    vld1q_u32({1,0,0,0}),
    vld1q_u32({2,0,0,0}),
    vld1q_u32({3,0,0,0})
};

It resulted in:

chacha_simd.cpp(309) : warning C4002: too many actual parameters for macro 'vld1
q_u32'
chacha_simd.cpp(309) : error C2143: syntax error : missing '}' before ')'
chacha_simd.cpp(309) : error C2664: 'const __n64 *__uint32ToN64_c(const uint32_t
 *)' : cannot convert argument 1 from 'initializer-list' to 'const uint32_t *'
        Reason: cannot convert from 'int' to 'const uint32_t *'
        Conversion from integral type to pointer type requires reinterpret_cast,
 C-style cast or function-style cast
chacha_simd.cpp(309) : error C2660: '__neon_Q1Adr' : function does not take 1 ar
guments
chacha_simd.cpp(310) : warning C4002: too many actual parameters for macro 'vld1
q_u32'
chacha_simd.cpp(310) : error C2143: syntax error : missing '}' before ')'
chacha_simd.cpp(310) : error C2664: 'const __n64 *__uint32ToN64_c(const uint32_t
 *)' : cannot convert argument 1 from 'initializer-list' to 'const uint32_t *'
        Reason: cannot convert from 'int' to 'const uint32_t *'
        Conversion from integral type to pointer type requires reinterpret_cast,
 C-style cast or function-style cast
chacha_simd.cpp(310) : error C2660: '__neon_Q1Adr' : function does not take 1 ar
guments
chacha_simd.cpp(310) : fatal error C1903: unable to recover from previous error(
s); stopping compilation

According to some source code for arm_neon.h on GitHub, __neon_Q1Adr and vld1q_u32 are:

__n128 __neon_Q1Adr(unsigned int, const __n64*);

#define vld1q_u32(pcD) ( __neon_Q1Adr( 0xf4200a8f, __uint32ToN64_c(pcD)) )

Things are just getting messier. Searching for "arm initialize "uint32x4_t" site:microsoft.com" and "arm initialize "uint32x4_t" site:msdn.com" are returning 0 hits.

How does one initialize a uint32x4_t using Microsoft compilers?


Solution

  • Jake's answer portably compiles, but (like for x86 intrinsics), compilers are stupid and actually copy the array at run-time when you use an intrinsic as a static initializer. (Either inside a function, or once in a constructor-like static initializer.) It would be more efficient to write code that indexed the underlying array of scalars like vld1q_u32(&array[idx*4])


    The winddk-8.1 header you linked, arm_neon.h, pretty clearly shows typedef __n128 uint32x4_t; (same as other element widths for 128-bit vectors), and that the underlying __n128 type is defined as a union with the __int64[2] member first.

    typedef union __declspec(intrin_type) _ADVSIMD_ALIGN(8) __n128
    {
         unsigned __int64   n128_u64[2];
         unsigned __int32   n128_u32[4];
         unsigned __int16   n128_u16[8];
         unsigned __int8    n128_u8[16];
         __int64            n128_i64[2];
         __int32            n128_i32[4];
         __int16            n128_i16[8];
         __int8             n128_i8[16];
         float              n128_f32[4];
    
        struct
        {
            __n64  low64;
            __n64  high64;
        } DUMMYNEONSTRUCT;
    
    } __n128;
    

    If you want to write MSVC-only code that depends on header internals, you can simply combine pairs of 32-bit integers into 64-bit integers. For little-endian ARM, this means making the 2nd 32-bit element the high 32-bits of a combined 64-bit element.

    #ifdef _MSC_VER
    // MSVC only; will silently compile differently on others
    static const uint32x4_t CTRS[3] = {
         // The .n128_u64 field is first in the definition of uint32x4_t
         {1 + (0ULL<<32), 0 + (0ULL<<32)},   // ARM is little-endian
         {2 + (0ULL<<32), 0 + (0ULL<<32)},
         {3 + (0ULL<<32), 0 + (0ULL<<32)},
    };
    

    We can wrap this up with a CPP macro to make it portable between compilers

    I made one macro for the whole uint32x4_t, rather than a pair macro you could also use for 64-bit vectors. This makes the actual declarations less of a mess of braces and macro names, because we can include the outer {} in this macro.

    #ifdef _MSC_VER
      // The .n128_u64 field is first.  Combine pairs of 32-bit integers in little-endian order.
    #define INITu32x4(w,x,y,z) { ((w) + (unsigned long long(x) << 32)), ((y) + (unsigned long long(z) << 32)) }
    #else
    #define INITu32x4(w,x,y,z) { (w), (x), (y), (z) }
    #endif
    
    static const uint32x4_t CTRS[3] = {
     INITu32x4(1,0,0,0),
     INITu32x4(2,0,0,0),
     INITu32x4(3,0,0,0),
    };
    

    The compiles correctly+efficiently on GCC and MSVC to the right data in the read-only data section (.rodata or .rdata), with no runtime initialization. From the Godbolt compiler explorer:

    uint32x4_t access(int idx) {
      return CTRS[idx];
    }
    

    @ g++5.4 -O3 -Wall -mcpu=cortex-a53 -mfpu=neon -mfloat-abi=hard -std=gnu++11
    access(int):
        movw    r3, #:lower16:.LANCHOR0
        movt    r3, #:upper16:.LANCHOR0    @ gcc chooses to construct the address with movw/movt
                                           @ instead of loading from a literal pool when optimizing for cortex-a53
        add     r0, r3, r0, lsl #4
        vld1.64 {d0-d1}, [r0:64]
        bx      lr
    
        .section        .rodata
        .align  3
        .set    .LANCHOR0,. + 0         @@ equivalent to .LANCHOR0: here. 
                @@ Reference point that could be used for other .rodata objects if needed.
        .type   CTRS, %object
        .size   CTRS, 48
    CTRS:
        .word   1
        .word   0
        .word   0
        .word   0
        .word   2
        .word   0
         ...
    

    And MSVC -Ox: I have no idea why MSVC's DCQ directive still needs 2 args to construct a single 64-bit value, exactly the same as DCD if you make an array of int. That seems to be different from Keil's DCQ directive / pseudo-instruction where each comma-separated arg is a 64-bit integer.

    But AFAICT, the comments MSVC added are an accurate representation of the number for each line.

     ;; ARM msvc19.14 -O2
        .rdata
    |__n128 const * const CTRS| DCQ 0x1, 0x0           ;  = 0x0000000000000001 ; CTRS
            DCQ     0x0, 0x0                      ;  = 0x0000000000000000
            DCQ     0x2, 0x0                      ;  = 0x0000000000000002
            DCQ     0x0, 0x0                      ;  = 0x0000000000000000
            DCQ     0x3, 0x0                      ;  = 0x0000000000000003
            DCQ     0x0, 0x0                      ;  = 0x0000000000000000
            EXPORT  |__n128 access(int)|   ; access
    
    .text$mn        SEGMENT
    
    |__n128 access(int)| PROC                        ; access
            movw        r3,|__n128 const * const CTRS|
            movt        r3,|__n128 const * const CTRS|
            add         r3,r3,r0,lsl #4
            vldm        r3,{d0,d1}
    |$M4|
            bx          lr
    
            ENDP  ; |__n128 access(int)|, access
    

    In C (but not C++), MSVC allows designated-initializer syntax

    static const uint32x4_t CTRS[3] = { [0].n128_u32 = {1, 0, 0, 0}, [1].n128_u32 = {2, 0, 0, 0}, [2].n128_u32 = {3, 0, 0, 0} };
    
    uint32x4_t access(int idx) {
      return CTRS[idx];
    }
    

    This compiles just fine in MSVC's C mode, but not C++. You could use this for a slightly more future-proof definition of INITu32x4 which fails noisily if something is wrong, and won't break if MS decides to reorder the union definition.

    Godbolt has a C language mode. I usually never use it (and just use -xc for g++/clang++) because it's inconvenient to switch between the two, but I don't know a command-line option to make MSVC compile as C. Anyway, this on Godbolt.