C++中高效的整数向下取整函数

34

我想定义一个高效的整数向下取整函数,即从浮点数或双精度浮点数进行向下截断的转换。

我们可以假设这些值不会导致整数溢出。到目前为止,我有几个选项:

  • 将其转换为int;这需要特殊处理负值,因为转换向零截断;

    I= int(F); if (I < 0 && I != F) I--;
    
  • 将 floor 的结果转换为 int;

  • int(floor(F));
    
  • 使用大位移转换为整数以获得正数(对于大值可能返回错误的结果);

  • int(F + double(0x7fffffff)) - 0x7fffffff;
    

将类型转换为整数通常会很慢。条件测试也是如此。我没有计时地板函数,但看到过一些帖子声称它也很慢。

你能想到更好的替代方式吗,以提高速度、准确性或允许范围?不需要可移植性。目标是最近的x86/x64架构。


评论不适合进行长时间的讨论;此对话已被移至聊天室 - Samuel Liew
@ilkkachu:请阅读评论和答案。还有其他选择。 - user1196549
5个回答

45
将int转换的速度慢得出了名。也许你自从x86-64以来一直生活在岩石下,或者错过了这个事实,即在x86上已经有一段时间没有这种情况了。SSE/SSE2有一个指令可以进行截断转换(而不是默认的四舍五入模式)。ISA支持这种操作的效率,正是因为在实际代码库中,具有C语义的转换并不罕见。x86-64代码使用SSE/SSE2 XMM寄存器进行标量FP数学运算,而不是使用x87,因为这样做更有效率。即使是现代的32位代码也使用XMM寄存器进行标量数学计算。
当编译x87(没有SSE3 fisttp)时,编译器过去必须改变x87的舍入模式以进行截断,将FP存储到内存中,然后再将舍入模式改回来。(如果需要进一步处理,则通常会从堆栈上的本地重新加载整数。)x87对此非常糟糕。
是的,在2006年@Kirjain的答案中提到的链接中,这确实非常慢,例如如果您仍然拥有32位CPU或使用x86-64 CPU运行32位代码。
除了截断或默认(最近)的舍入模式外,直接不支持其他舍入模式的转换。 在SSE4.1之前,您最好使用类似于@ Kirjain答案中的2006年链接的魔术数字技巧。
那里有一些很好的技巧,但仅适用于double-> 32位整数。 如果有float,则不太可能扩展到double。
或更常见的是,只需添加一个大幅值来触发舍入,然后再次减去它以返回原始范围。 这对于float可以起作用,而无需扩展到double,但我不确定如何使floor正常工作。
无论如何,这里的明显解决方案是_mm256_floor_ps()_mm256_cvtps_epi32vroundpsvcvtps2dq)。这个非AVX版本可以使用SSE4.1。如果你有一个要处理的巨大数组(并且不能将这项工作与其他工作交错),你可以将MXCSR舍入模式设置为“向-Inf”(floor),然后简单地使用vcvtps2dq(它使用当前的舍入模式)。然后再将其设置回来。但最好将转换缓存块或在生成数据时即时进行转换,假定需要将FP舍入模式设置为默认的Nearest。

roundps/pd/ss/sd在Intel CPU上是2个uop,但在AMD Ryzen上是1个uop(每个128位通道)。cvtps2dq也是1个uop。打包的double->int转换还包括一个洗牌。标量FP->int转换(将其复制到整数寄存器)通常还需要额外的uop。

因此,在某些情况下,可能会有魔术数字技巧胜出的余地;如果_mm256_floor_ps()+ cvt是关键瓶颈的一部分(或更可能的是如果你有double并且想要int32),那么值得调查一下。


@Cássio Renan的int foo = floorf(f)如果使用gcc -O3 -fno-trapping-math(或-ffast-math)编译,并且使用具有SSE4.1或AVX的-march=,则实际上会自动向量化。https://godbolt.org/z/ae_KPv

如果您使用其他未手动向量化的标量代码,则可能会很有用。特别是如果您希望编译器自动向量化整个代码。


7
这是一个非常详细的回答,尽管关于“生活在岩石下面”的问题可能比你想象的要严厉一些。 - Davislor
8
@Davislor在这段话中表示他的本意是有点刻薄的,他希望通过这种方式唤醒那些“知识”的人们,让他们认识到他们所拥有的知识已经过时,需要重新审视。此外,Yves之前曾发布过一些有关x86指令集向量化的问题,因此他很惊讶为什么Yves不知道编译器如何利用SSE2进行浮点数运算优化。 - Peter Cordes
7
@Davislor: “如果他已经知道” - 不,这不是整个问题。将 C 强制转换为 int 会向 0 截断,但 Yves 想要的是 floor。这里仍然有一个完全真实的问题,知道 C 强制转换很有效并不能回答它。因此,我认为这不是粗鲁或不友好的,它用幽默的方式表达了依赖旧性能“事实”的观点。我认为大多数读者不会将其视为真正的侮辱,所以我认为这是一种有趣的介绍一个假设已经过时的事实的方式。 - Peter Cordes
好的,如果这是一个幽默的说法并且被当做笑话来理解的话。 - Davislor
6
@PeterCordes: 是的,我一直与世隔绝。出于业务原因,我一直支持VS 2008,最近才放弃了VS 2005。对于VB6的请求似乎有所减少了。 :-) - user1196549
显示剩余2条评论

18

看一下魔数。该网页提出的算法应该比简单转换更有效率。我自己从未使用过,但这是他们在该网站上提供的性能比较(xs_ToInt和xs_CRoundToInt是所提议的函数):

Performing 10000000 times:
simple cast           2819 ms i.e. i = (long)f;
xs_ToInt              1242 ms i.e. i = xs_ToInt(f); //numerically same as above
bit-twiddle(full)     1093 ms i.e. i = BitConvertToInt(f); //rounding from Fluid
fistp                  676 ms i.e. i = FISTToInt(f); //Herf, et al x86 Assembly rounding 
bit-twiddle(limited)   623 ms i.e. i = FloatTo23Bits(f); //Herf, rounding only in the range (0...1]  
xs_CRoundToInt         609 ms i.e. i = xs_CRoundToInt(f); //rounding with "magic" numbers

此外,xs_ToInt 显然被修改以提高性能:

Performing 10000000 times:
simple cast convert   3186 ms i.e. fi = (f*65536);
fistp convert         3031 ms i.e. fi = FISTToInt(f*65536);
xs_ToFix               622 ms i.e. fi = xs_Fix<16>::ToFix(f);

“魔法数字”方法的简要解释:

“基本上,为了将两个浮点数相加,处理器会‘对齐’这些数字的小数点,以便可以轻松地相加这些位。它通过“规范化”这些数字来实现,以保留最重要的位,即较小的数字“规范化”成与更大一些的数字相同。因此,xs_CRoundToInt()使用的“魔法数字”转换原理是:我们添加一个足够大的浮点数(一个数字如此之大,只有小数点之前有有效数字,之后没有),以使得:(a) 处理器将该数字规范化为其整数等效项;(b) 将两个数字相加时不会抹掉您尝试转换的数字中的整数有效位(即 XX00 + 00YY = XXYY)。”

这段引用摘自同一网页。


22
这段话是从一篇文章中摘录出来的,其中描述了一个13岁的编译器为Pentium 4生成32位代码的情况。这篇文章非常好,但只有部分内容在当前x86-64架构上仍然相关,因为现在标量FP数学运算是在XMM寄存器上使用SSE2进行,而不是在x87上进行。如果simple_cast比SSSE3 fisttp或基线x87 fistp慢,那么你的x86-64编译器有问题。 - Peter Cordes
3
如果你已经使用了float,将其扩展为double可能并不是一个好的选择。 - Peter Cordes
8
顺便说一下,链接文章中的代码不是可移植的C++代码。它有严格别名未定义行为,并且只在MSVC上安全。使用memcpy进行类型转换比较安全。(或者在所有x86-64编译器上,union也被支持,不像指针强制转换。) - Peter Cordes
3
当然,您必须通过值访问联合成员,而不是通过别名指针进行访问。严格的类型别名违规有时可能不会在GCC/clang上出错;是的,指针目标的可见性可能会使其在当前的GCC版本中起作用,但不能保证在未来的版本中。但是,类型转换基本上是一个已解决的问题,应该封装在一个函数中;您可以找到可移植的安全定义以编译出高效的代码。(虽然这种情况下获取64位双精度浮点数的低32位存在效率问题,特别是对于32位代码。) - Peter Cordes
7
很抱歉,这个答案基本上只是一个链接,因为它没有包含任何代码。请同时包括相关的代码(至少包括最佳答案的代码),以防止链接失效。 - Konrad Rudolph
显示剩余3条评论

4

如果你批量处理,编译器可能会自动矢量化,如果你知道该做什么。例如,这里是一个小的实现,它可以在 GCC 上自动矢量化将浮点数转换为整数:

#include <cmath>

// Compile with -O3 and -march=native to see autovectorization
__attribute__((optimize("-fno-trapping-math")))
void testFunction(float* input, int* output, int length) {
  // Assume the input and output are aligned on a 32-bit boundary.
  // Of course, you have  to ensure this when calling testFunction, or else
  // you will have problems.
  input = static_cast<float*>(__builtin_assume_aligned(input, 32));
  output = static_cast<int*>(__builtin_assume_aligned(output, 32));

  // Also assume the length is a multiple of 32.
  if (length & 31) __builtin_unreachable();

  // Do the conversion
  for (int i = 0; i < length; ++i) {
    output[i] = floor(input[i]);
  }
}

这是针对x86-64(含AVX512指令集)生成的汇编代码:

testFunction(float*, int*, int):
        test    edx, edx
        jle     .L5
        lea     ecx, [rdx-1]
        xor     eax, eax
.L3:
        # you can see here that the conversion was vectorized
        # to a vrndscaleps (that will round the float appropriately)
        # and a vcvttps2dq (thal will perform the conversion)
        vrndscaleps     ymm0, YMMWORD PTR [rdi+rax], 1
        vcvttps2dq      ymm0, ymm0
        vmovdqa64       YMMWORD PTR [rsi+rax], ymm0
        add     rax, 32
        cmp     rax, rdx
        jne     .L3
        vzeroupper
.L5:
        ret

如果您的目标不支持AVX512,它仍将使用SSE4.1指令进行自动向量化,假设您有这些内容。这是使用-O3 -msse4.1的输出:

testFunction(float*, int*, int):
        test    edx, edx
        jle     .L1
        shr     edx, 2
        xor     eax, eax
        sal     rdx, 4
.L3:
        roundps xmm0, XMMWORD PTR [rdi+rax], 1
        cvttps2dq       xmm0, xmm0
        movaps  XMMWORD PTR [rsi+rax], xmm0
        add     rax, 16
        cmp     rax, rdx
        jne     .L3
.L1:
        ret

在godbolt上实时查看


1
我甚至能够进行显式向量化(数字不连续)。一个重要的特性是截断方向正确。 - user1196549
1
@YvesDaoust 当然。我调整了代码以进行取整,就像你期望的那样。你可以看到编译器非常聪明,能够将其向量化。 也许你可以将浮点数打包到一个内存对齐的数组中,然后调用这个函数,再将它们解包回到你想要它们的地方。即使你有更快的算法,这仍然可能比逐个处理它们更快。尝试并测量一下是否有效(我无法做到这一点,因为我无法猜测你的机器/架构是什么样子的)。 - Cássio Renan
1
vrndscaless: 开启 AVX512F 编译似乎有点不寻常,值得一提!!!(使用 -march=skylake 可以获得普通的 AVX vroundss https://godbolt.org/z/k5NuI3)。但更重要的是,**这不是自动向量化的**。您有一个循环,使用标量单精度 (...ss) 指令每次处理 1 个浮点数。我不确定 为什么 gcc 无法自动向量化。使用 -fno-trapping-math 就可以实现向量化,所以我猜它认为需要处理精确异常,让您找出哪个数组元素引发了 FP 异常。https://godbolt.org/z/9kWwIJ - Peter Cordes
“gcc -fno-trapping-math”是什么意思?(//stackoverflow.com/q/50374771)表明,trapping-math会阻止FP数学运算被优化掉,以便在某些情况下读取FP状态标志以检测发生了哪些异常。这听起来很正确。而且,“roundps”特别抑制精度/不准确性异常(https://www.felixcloutier.com/x86/roundpd),因此在使用trapping-math时,汇编代码必须在C源代码会设置异常标志的情况下设置异常标志,而在不需要设置异常标志的情况下则不需要设置。(但多次设置也可以。)但如果标量可行,则向量化也应该可行:错过的优化。 - Peter Cordes
1
关于-march=native的解释在推荐使用时有些道理,但对于你在回答中引用的汇编代码来说却没有意义。你没有提到它在没有AVX512的情况下仍然可以工作。也许应该说“需要在支持SSE4.1的CPU上使用-march=native”,并展示-march=nehalem或core2 + -msse4.1的汇编输出。仅仅展示-march=skylake-avx512的输出并说“这是x86-64的生成汇编”是极其误导人的,因为那不是基线x86-64。它确实需要SSE4.1,而不是每个项目都能为他们分发的二进制文件启用的。 - Peter Cordes
显示剩余8条评论

2
为什么不直接使用这个:

最初的回答:
#include <cmath>

auto floor_(float const x) noexcept
{
  int const t(x);

  return t - (t > x);
}

这不够高效。 - 1201ProgramAlarm
为什么不行呢?因为在x86-64上,这种方式的编译效率不如floorf。如果没有SSE4.1(就像你链接的那样),它必须做很多工作来实现trunc而不进行有限范围整数的转换。即使使用了-march=nehalem,它也会分支。在转换为整数后执行条件子操作可能会有所帮助。此外,您的函数返回一个float而不是int,因此它甚至不能满足问题的要求。 - Peter Cordes
1
尝试修复它。在 Nehalem 下不再分支。 - user1095108
这在自动向量化时可能不会太糟糕,但对于标量而言并不好。它将整数转换回浮点数,这会导致在Intel上的cvtsi2ss中花费2个uops。如果您不能假设SSE4.1,则使用打包转换的SIMD版本可能很好。(整数和返回的打包转换仅总共需要2个uops,并且SIMD比较产生一个0 / -1整数位模式,您可以简单地addps。)但仍然不如SSE4.1 roundps + cvtps2dq高效。 - Peter Cordes
1
嗯,楼主忽略了一个事实,即 if 并不是真正需要的,因此也许这个答案是值得的。 - user1095108

1
这是对Cássio Renan优秀回答的修改。它用标准的C++替换了所有编译器特定的扩展,理论上可在任何符合规范的编译器中移植。此外,它检查参数是否正确对齐,而不是假设它们已经对齐。它优化到相同的代码。
#include <assert.h>
#include <cmath>
#include <stddef.h>
#include <stdint.h>

#define ALIGNMENT alignof(max_align_t)
using std::floor;

// Compiled with: -std=c++17 -Wall -Wextra -Wpedantic -Wconversion -fno-trapping-math -O -march=cannonlake -mprefer-vector-width=512

void testFunction(const float in[], int32_t out[], const ptrdiff_t length)
{
  static_assert(sizeof(float) == sizeof(int32_t), "");
  assert((uintptr_t)(void*)in % ALIGNMENT == 0);
  assert((uintptr_t)(void*)out % ALIGNMENT == 0);
  assert((size_t)length % (ALIGNMENT/sizeof(int32_t)) == 0);

  alignas(ALIGNMENT) const float* const input = in;
  alignas(ALIGNMENT) int32_t* const output = out;

  // Do the conversion
  for (int i = 0; i < length; ++i) {
    output[i] = static_cast<int32_t>(floor(input[i]));
  }
}

这段代码在GCC上优化效果不如使用非可移植扩展的原始代码。C++标准确实支持alignas限定符、对齐数组的引用以及返回缓冲区内对齐范围的std::align函数。然而,在我测试过的所有编译器中,这些都不能使编译器生成对齐而不是未对齐的向量加载和存储。
尽管在x86_64上alignof(max_align_t)仅为16,并且可以将ALIGNMENT定义为常量64,但这并不能帮助任何编译器生成更好的代码,因此我选择了可移植性。最接近强制编译器假定指针对齐的可移植方式是使用<immintrin.h>中的类型,这是大多数x86编译器支持的,或者定义一个带有alignas说明符的struct。通过检查预定义宏,您还可以在Linux编译器上将宏扩展为__attribute__ ((aligned (ALIGNMENT))),或在Windows编译器上扩展为__declspec (align (ALIGNMENT)),并在我们不知道的编译器上使用一些安全的东西,但GCC需要在type上使用属性来实际生成对齐的加载和存储。
此外,原始示例调用了一个内置函数,告诉GCC不可能出现length不是32的倍数。如果您使用assert()或调用标准函数(如abort()),GCC、Clang和ICC都不会做出相同的推断。因此,它们生成的大多数代码将处理length不是向量宽度的整数倍的情况。
这样做的一个可能原因是,优化并不能带来太多速度提升:在Intel CPU上,具有对齐地址的非对齐内存指令速度很快,并且处理length不是整数倍的情况的代码只需几个字节的长度,在常数时间内运行。
值得一提的是,GCC能够更好地优化<cmath>中的内联函数,而不是<math.c>中实现的宏。
GCC 9.1需要一组特定的选项来生成AVX512代码。即使使用"-march=cannonlake",默认情况下它也会倾向于256位向量。它需要"-mprefer-vector-width=512"来生成512位代码。(感谢Peter Cordes指出这一点)。它在向量化循环后跟随展开代码,以转换数组中任何剩余的元素。
这是向量化主循环,减去了一些常量时间初始化、错误检查和清理代码,这些代码只会运行一次:
.L7:
        vrndscaleps     zmm0, ZMMWORD PTR [rdi+rax], 1
        vcvttps2dq      zmm0, zmm0
        vmovdqu32       ZMMWORD PTR [rsi+rax], zmm0
        add     rax, 64
        cmp     rax, rcx
        jne     .L7

机警的读者会注意到Cássio Renan程序生成的代码与此处有两个不同之处:它使用%zmm而不是%ymm寄存器,并且使用不对齐的vmovdqu32而不是对齐的vmovdqa64来存储结果。
使用相同的标志,Clang 8.0.0在展开循环方面做出了不同选择。每次迭代操作八个512位向量(即128个单精度浮点数),但获取剩余部分的代码没有被展开。如果至少还有64个浮点数,它将使用另外四个AVX512指令处理这些,然后通过非矢量化循环清理任何额外的余数。
如果您在Clang++中编译原始程序,它将不会抱怨并接受它,但不会进行相同的优化:它仍然不会假设length是向量宽度的倍数,也不会假设指针是对齐的。
它更喜欢AVX512代码而不是AVX256,即使没有使用-mprefer-vector-width=512
        test    rdx, rdx
        jle     .LBB0_14
        cmp     rdx, 63
        ja      .LBB0_6
        xor     eax, eax
        jmp     .LBB0_13
.LBB0_6:
        mov     rax, rdx
        and     rax, -64
        lea     r9, [rax - 64]
        mov     r10, r9
        shr     r10, 6
        add     r10, 1
        mov     r8d, r10d
        and     r8d, 1
        test    r9, r9
        je      .LBB0_7
        mov     ecx, 1
        sub     rcx, r10
        lea     r9, [r8 + rcx]
        add     r9, -1
        xor     ecx, ecx
.LBB0_9:                                # =>This Inner Loop Header: Depth=1
        vrndscaleps     zmm0, zmmword ptr [rdi + 4*rcx], 9
        vrndscaleps     zmm1, zmmword ptr [rdi + 4*rcx + 64], 9
        vrndscaleps     zmm2, zmmword ptr [rdi + 4*rcx + 128], 9
        vrndscaleps     zmm3, zmmword ptr [rdi + 4*rcx + 192], 9
        vcvttps2dq      zmm0, zmm0
        vcvttps2dq      zmm1, zmm1
        vcvttps2dq      zmm2, zmm2
        vmovups zmmword ptr [rsi + 4*rcx], zmm0
        vmovups zmmword ptr [rsi + 4*rcx + 64], zmm1
        vmovups zmmword ptr [rsi + 4*rcx + 128], zmm2
        vcvttps2dq      zmm0, zmm3
        vmovups zmmword ptr [rsi + 4*rcx + 192], zmm0
        vrndscaleps     zmm0, zmmword ptr [rdi + 4*rcx + 256], 9
        vrndscaleps     zmm1, zmmword ptr [rdi + 4*rcx + 320], 9
        vrndscaleps     zmm2, zmmword ptr [rdi + 4*rcx + 384], 9
        vrndscaleps     zmm3, zmmword ptr [rdi + 4*rcx + 448], 9
        vcvttps2dq      zmm0, zmm0
        vcvttps2dq      zmm1, zmm1
        vcvttps2dq      zmm2, zmm2
        vcvttps2dq      zmm3, zmm3
        vmovups zmmword ptr [rsi + 4*rcx + 256], zmm0
        vmovups zmmword ptr [rsi + 4*rcx + 320], zmm1
        vmovups zmmword ptr [rsi + 4*rcx + 384], zmm2
        vmovups zmmword ptr [rsi + 4*rcx + 448], zmm3
        sub     rcx, -128
        add     r9, 2
        jne     .LBB0_9
        test    r8, r8
        je      .LBB0_12
.LBB0_11:
        vrndscaleps     zmm0, zmmword ptr [rdi + 4*rcx], 9
        vrndscaleps     zmm1, zmmword ptr [rdi + 4*rcx + 64], 9
        vrndscaleps     zmm2, zmmword ptr [rdi + 4*rcx + 128], 9
        vrndscaleps     zmm3, zmmword ptr [rdi + 4*rcx + 192], 9
        vcvttps2dq      zmm0, zmm0
        vcvttps2dq      zmm1, zmm1
        vcvttps2dq      zmm2, zmm2
        vcvttps2dq      zmm3, zmm3
        vmovups zmmword ptr [rsi + 4*rcx], zmm0
        vmovups zmmword ptr [rsi + 4*rcx + 64], zmm1
        vmovups zmmword ptr [rsi + 4*rcx + 128], zmm2
        vmovups zmmword ptr [rsi + 4*rcx + 192], zmm3
.LBB0_12:
        cmp     rax, rdx
        je      .LBB0_14
.LBB0_13:                               # =>This Inner Loop Header: Depth=1
        vmovss  xmm0, dword ptr [rdi + 4*rax] # xmm0 = mem[0],zero,zero,zero
        vroundss        xmm0, xmm0, xmm0, 9
        vcvttss2si      ecx, xmm0
        mov     dword ptr [rsi + 4*rax], ecx
        add     rax, 1
        cmp     rdx, rax
        jne     .LBB0_13
.LBB0_14:
        pop     rax
        vzeroupper
        ret
.LBB0_7:
        xor     ecx, ecx
        test    r8, r8
        jne     .LBB0_11
        jmp     .LBB0_12

ICC 19同样生成AVX512指令,但与clang非常不同。它使用了更多的魔法常数进行设置,但不会展开任何循环,而是操作512位向量。

此代码还适用于其他编译器和架构。(尽管MSVC仅支持到AVX2 ISA,并且无法自动向量化循环。)例如,在带有-march=armv8-a+simd的ARM上,它将生成一个矢量化循环,其中包括frintm v0.4s,v0.4sfcvtzs v0.4s,v0.4s

请自行尝试


1
assert((uintptr_t)(void*)in % alignof(max_align_t) == 0);__builtin_assume_aligned 是非常不同的。如果你使用 -DNDEBUG 编译,assert 将会消失,并且 不会 告诉编译器你的数据实际上是对齐的。或者在没有 NDEBUG 的情况下,它实际上会发出指令来检查运行时的对齐方式。但是 __builtin_assume_aligned 是对编译器的一个 承诺,即你的数据已经对齐了。在这种情况下,基本上不需要使用 AVX,因为对齐并不能使自动向量化更好(只要你使用的是 gcc8 或更高版本;早期的 GCC 将检查对齐并达到边界)。 - Peter Cordes
@PeterCordes 这段代码如果使用-DNDEBUG编译将会表现得更差。默认情况下,它的优化效果一样好,并且更具可移植性,错误信息也更有用。请随意使用您喜欢的编码风格,我会做个记录。 - Davislor
你的方式是一个有效的选择;我反对的是这个重写使用纯ISO C++的暗示是一个简单的移植。你的更新解决了这个问题。但是,无论如何,使用clang或gcc8/9,-DNDEBUG都不会使它变慢。https://godbolt.org/z/-6xKOG显示,使用clang8省略断言只是在开头省略了一些cmp/branch,而实际工作在主循环和清理中是相同的。你只承诺16字节的对齐,所以clang已经在执行未对齐的加载/存储(如果数据在运行时对齐,则没有额外的成本)。 - Peter Cordes
我不知道在ISO C++中承诺编译器对齐的可移植方式 :( 也许可以通过转换为数组类型来实现?当我尝试过一些东西后,最终得到的是一个浮点数数组,其中每个元素都填充到16字节,而不仅仅是数组的开头。(就像使用typedef定义对齐浮点类型一样)。然而,ISO C++确实允许您对数组进行alignas(),而无需扩展每个元素。 - Peter Cordes
@PeterCordes 好的,我明白了。 - Davislor
显示剩余8条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接