在 `C` 函数中定义 `static const` SIMD 变量
Define a `static const` SIMD Variable within a `C` Function
我有一个这种形式的函数(来自 ):
__m128 FastExpSse(__m128 x)
{
static __m128 const a = _mm_set1_ps(12102203.2f); // (1 << 23) / ln(2)
static __m128i const b = _mm_set1_epi32(127 * (1 << 23) - 486411);
static __m128 const m87 = _mm_set1_ps(-87);
// fast exponential function, x should be in [-87, 87]
__m128 mask = _mm_cmpge_ps(x, m87);
__m128i tmp = _mm_add_epi32(_mm_cvtps_epi32(_mm_mul_ps(a, x)), b);
return _mm_and_ps(_mm_castsi128_ps(tmp), mask);
}
我想让它 C
兼容。
然而,当我使用 C
编译器时,编译器不接受 static __m128i const b = _mm_set1_epi32(127 * (1 << 23) - 486411);
形式。
但我不希望在每次函数调用时重新计算前 3 个值。
一种解决方案是将其内联(但有时编译器会拒绝这样做)。
是否有 C
样式来实现以防函数未内联?
谢谢。
此解决方案显然不可移植,它适用于 GCC 8(仅使用此编译器测试):
#include <stdio.h>
#include <stdint.h>
#include <emmintrin.h>
#include <string.h>
#define INIT_M128(vFloat) {(vFloat), (vFloat), (vFloat), (vFloat)}
#define INIT_M128I(vU32) {((uint64_t)(vU32) | (uint64_t)(vU32) << 32u), ((uint64_t)(vU32) | (uint64_t)(vU32) << 32u)}
static void print128(const void *p)
{
unsigned char buf[16];
memcpy(buf, p, 16);
for (int i = 0; i < 16; ++i)
{
printf("%02X ", buf[i]);
}
printf("\n");
}
int main(void)
{
static __m128 const glob_a = INIT_M128(12102203.2f);
static __m128i const glob_b = INIT_M128I(127 * (1 << 23) - 486411);
static __m128 const glob_m87 = INIT_M128(-87.0f);
__m128 a = _mm_set1_ps(12102203.2f);
__m128i b = _mm_set1_epi32(127 * (1 << 23) - 486411);
__m128 m87 = _mm_set1_ps(-87);
print128(&a);
print128(&glob_a);
print128(&b);
print128(&glob_b);
print128(&m87);
print128(&glob_m87);
return 0;
}
正如@harold 的回答中所解释的(仅在 C 中),以下代码(使用或不使用 WITHSTATIC
构建)生成完全相同的代码。
#include <stdio.h>
#include <stdint.h>
#include <emmintrin.h>
#include <string.h>
#define INIT_M128(vFloat) {(vFloat), (vFloat), (vFloat), (vFloat)}
#define INIT_M128I(vU32) {((uint64_t)(vU32) | (uint64_t)(vU32) << 32u), ((uint64_t)(vU32) | (uint64_t)(vU32) << 32u)}
__m128 FastExpSse2(__m128 x)
{
#ifdef WITHSTATIC
static __m128 const a = INIT_M128(12102203.2f);
static __m128i const b = INIT_M128I(127 * (1 << 23) - 486411);
static __m128 const m87 = INIT_M128(-87.0f);
#else
__m128 a = _mm_set1_ps(12102203.2f);
__m128i b = _mm_set1_epi32(127 * (1 << 23) - 486411);
__m128 m87 = _mm_set1_ps(-87);
#endif
__m128 mask = _mm_cmpge_ps(x, m87);
__m128i tmp = _mm_add_epi32(_mm_cvtps_epi32(_mm_mul_ps(a, x)), b);
return _mm_and_ps(_mm_castsi128_ps(tmp), mask);
}
所以总而言之,最好删除 static 和 const 关键字(C++ 中的代码更好更简单,而 C 中的代码是可移植的,因为我提出的 hack 代码并不是真正可移植的)
删除 static
和 const
。
同时从 C++ 版本中删除它们。 const
还可以,但是 static
太可怕了,引入了每次都检查的保护变量,并且第一次初始化非常昂贵。
__m128 a = _mm_set1_ps(12102203.2f);
不是函数调用,它只是表达向量常量的一种方式。 "doing it only once" 无法节省时间 - 它通常发生 零 次,常量向量在程序的数据段中准备好并在运行时简单地加载,没有static
介绍的围绕它的垃圾。
检查 asm 以确保没有 static
会发生这种情况:(from godbolt)
FastExpSse(float __vector(4)):
movaps xmm1, XMMWORD PTR .LC0[rip]
cmpleps xmm1, xmm0
mulps xmm0, XMMWORD PTR .LC1[rip]
cvtps2dq xmm0, xmm0
paddd xmm0, XMMWORD PTR .LC2[rip]
andps xmm0, xmm1
ret
.LC0:
.long 3266183168
.long 3266183168
.long 3266183168
.long 3266183168
.LC1:
.long 1262004795
.long 1262004795
.long 1262004795
.long 1262004795
.LC2:
.long 1064866805
.long 1064866805
.long 1064866805
.long 1064866805
_mm_set1_ps(-87);
或任何其他 _mm_set
内在函数不是当前编译器的有效静态初始值设定项,因为它不被视为 constant expression.
在 C++ 中,它编译为 static
存储位置的运行时初始化(从其他地方的矢量文字复制)。如果它是函数内部的 static __m128
,则有一个保护变量来保护它。
在 C 中,它只是拒绝编译,因为 C 不支持非常量初始化器/构造器。 _mm_set
不像@benjarobin 的回答显示的底层 GNU C 本机向量的花括号初始化器。
这真的很愚蠢,而且似乎是所有 4 个主流 x86 C++ 编译器 (gcc/clang/ICC/MSVC) 中的优化失误。即使每个 static const __m128 var
都有一个不同的地址在某种程度上很重要,编译器也可以通过使用初始化的只读存储而不是在运行时复制来实现这一点。
因此,即使启用了优化,常量传播似乎也无法将 _mm_set
转变为常量初始值设定项。
即使在 C++ 中也不要使用 static const __m128 var = _mm_set...
;效率低下。
函数内部更糟,但全局范围仍然很糟糕。
相反,请避免 static
。您仍然可以使用 const
来阻止自己不小心分配其他内容,并告诉人类读者它是一个常量。没有 static
,它不会影响 where/how 你的变量被存储。 const
自动存储只是在编译时检查你没有修改对象。
const __m128 var = _mm_set1_ps(-87); // not static
编译器擅长于此,将优化多个函数使用相同向量常量的情况,就像它们删除重复字符串文字和将它们放在只读存储器中。
在小辅助函数中以这种方式定义常量很好:在内联函数后,编译器会将常量设置提升到循环之外。
它还可以让编译器优化掉全部 16 字节的存储空间,并用 vbroadcastss xmm0, dword [mem]
或类似的东西加载它。
我有一个这种形式的函数(来自
__m128 FastExpSse(__m128 x)
{
static __m128 const a = _mm_set1_ps(12102203.2f); // (1 << 23) / ln(2)
static __m128i const b = _mm_set1_epi32(127 * (1 << 23) - 486411);
static __m128 const m87 = _mm_set1_ps(-87);
// fast exponential function, x should be in [-87, 87]
__m128 mask = _mm_cmpge_ps(x, m87);
__m128i tmp = _mm_add_epi32(_mm_cvtps_epi32(_mm_mul_ps(a, x)), b);
return _mm_and_ps(_mm_castsi128_ps(tmp), mask);
}
我想让它 C
兼容。
然而,当我使用 C
编译器时,编译器不接受 static __m128i const b = _mm_set1_epi32(127 * (1 << 23) - 486411);
形式。
但我不希望在每次函数调用时重新计算前 3 个值。
一种解决方案是将其内联(但有时编译器会拒绝这样做)。
是否有 C
样式来实现以防函数未内联?
谢谢。
此解决方案显然不可移植,它适用于 GCC 8(仅使用此编译器测试):
#include <stdio.h>
#include <stdint.h>
#include <emmintrin.h>
#include <string.h>
#define INIT_M128(vFloat) {(vFloat), (vFloat), (vFloat), (vFloat)}
#define INIT_M128I(vU32) {((uint64_t)(vU32) | (uint64_t)(vU32) << 32u), ((uint64_t)(vU32) | (uint64_t)(vU32) << 32u)}
static void print128(const void *p)
{
unsigned char buf[16];
memcpy(buf, p, 16);
for (int i = 0; i < 16; ++i)
{
printf("%02X ", buf[i]);
}
printf("\n");
}
int main(void)
{
static __m128 const glob_a = INIT_M128(12102203.2f);
static __m128i const glob_b = INIT_M128I(127 * (1 << 23) - 486411);
static __m128 const glob_m87 = INIT_M128(-87.0f);
__m128 a = _mm_set1_ps(12102203.2f);
__m128i b = _mm_set1_epi32(127 * (1 << 23) - 486411);
__m128 m87 = _mm_set1_ps(-87);
print128(&a);
print128(&glob_a);
print128(&b);
print128(&glob_b);
print128(&m87);
print128(&glob_m87);
return 0;
}
正如@harold 的回答中所解释的(仅在 C 中),以下代码(使用或不使用 WITHSTATIC
构建)生成完全相同的代码。
#include <stdio.h>
#include <stdint.h>
#include <emmintrin.h>
#include <string.h>
#define INIT_M128(vFloat) {(vFloat), (vFloat), (vFloat), (vFloat)}
#define INIT_M128I(vU32) {((uint64_t)(vU32) | (uint64_t)(vU32) << 32u), ((uint64_t)(vU32) | (uint64_t)(vU32) << 32u)}
__m128 FastExpSse2(__m128 x)
{
#ifdef WITHSTATIC
static __m128 const a = INIT_M128(12102203.2f);
static __m128i const b = INIT_M128I(127 * (1 << 23) - 486411);
static __m128 const m87 = INIT_M128(-87.0f);
#else
__m128 a = _mm_set1_ps(12102203.2f);
__m128i b = _mm_set1_epi32(127 * (1 << 23) - 486411);
__m128 m87 = _mm_set1_ps(-87);
#endif
__m128 mask = _mm_cmpge_ps(x, m87);
__m128i tmp = _mm_add_epi32(_mm_cvtps_epi32(_mm_mul_ps(a, x)), b);
return _mm_and_ps(_mm_castsi128_ps(tmp), mask);
}
所以总而言之,最好删除 static 和 const 关键字(C++ 中的代码更好更简单,而 C 中的代码是可移植的,因为我提出的 hack 代码并不是真正可移植的)
删除 static
和 const
。
同时从 C++ 版本中删除它们。 const
还可以,但是 static
太可怕了,引入了每次都检查的保护变量,并且第一次初始化非常昂贵。
__m128 a = _mm_set1_ps(12102203.2f);
不是函数调用,它只是表达向量常量的一种方式。 "doing it only once" 无法节省时间 - 它通常发生 零 次,常量向量在程序的数据段中准备好并在运行时简单地加载,没有static
介绍的围绕它的垃圾。
检查 asm 以确保没有 static
会发生这种情况:(from godbolt)
FastExpSse(float __vector(4)):
movaps xmm1, XMMWORD PTR .LC0[rip]
cmpleps xmm1, xmm0
mulps xmm0, XMMWORD PTR .LC1[rip]
cvtps2dq xmm0, xmm0
paddd xmm0, XMMWORD PTR .LC2[rip]
andps xmm0, xmm1
ret
.LC0:
.long 3266183168
.long 3266183168
.long 3266183168
.long 3266183168
.LC1:
.long 1262004795
.long 1262004795
.long 1262004795
.long 1262004795
.LC2:
.long 1064866805
.long 1064866805
.long 1064866805
.long 1064866805
_mm_set1_ps(-87);
或任何其他 _mm_set
内在函数不是当前编译器的有效静态初始值设定项,因为它不被视为 constant expression.
在 C++ 中,它编译为 static
存储位置的运行时初始化(从其他地方的矢量文字复制)。如果它是函数内部的 static __m128
,则有一个保护变量来保护它。
在 C 中,它只是拒绝编译,因为 C 不支持非常量初始化器/构造器。 _mm_set
不像@benjarobin 的回答显示的底层 GNU C 本机向量的花括号初始化器。
这真的很愚蠢,而且似乎是所有 4 个主流 x86 C++ 编译器 (gcc/clang/ICC/MSVC) 中的优化失误。即使每个 static const __m128 var
都有一个不同的地址在某种程度上很重要,编译器也可以通过使用初始化的只读存储而不是在运行时复制来实现这一点。
因此,即使启用了优化,常量传播似乎也无法将 _mm_set
转变为常量初始值设定项。
即使在 C++ 中也不要使用 static const __m128 var = _mm_set...
;效率低下。
函数内部更糟,但全局范围仍然很糟糕。
相反,请避免 static
。您仍然可以使用 const
来阻止自己不小心分配其他内容,并告诉人类读者它是一个常量。没有 static
,它不会影响 where/how 你的变量被存储。 const
自动存储只是在编译时检查你没有修改对象。
const __m128 var = _mm_set1_ps(-87); // not static
编译器擅长于此,将优化多个函数使用相同向量常量的情况,就像它们删除重复字符串文字和将它们放在只读存储器中。
在小辅助函数中以这种方式定义常量很好:在内联函数后,编译器会将常量设置提升到循环之外。
它还可以让编译器优化掉全部 16 字节的存储空间,并用 vbroadcastss xmm0, dword [mem]
或类似的东西加载它。