如何在C++中编写高效且无UB的vec3::operator[]?
自定义C++ vec3类的合规高性能实现探讨
免责声明
本问题更偏向于C++语言规则探讨,旨在探索编译器的能力边界,请勿过度纠结下述需求的必要性。
需求背景
要编写一个自定义vec3类,用数组存储三个float类型数值(数学意义上的向量),需满足以下要求:
- 可通过
v.x直接访问单个成员,而非v.x()这类访问器,以便与glm::vec3等类型互换使用。 - 需实现
operator[],返回对应维度的元素引用float&,适配支持“类数组”动态索引的通用场景。 operator[]的实现需尽可能高性能,可能每毫秒被调用数万次。- 需为平凡可复制类型,且
x、y、z地址连续,以便直接将vec3数组复制到GPU内存供着色器使用。 operator[]的实现不能存在未定义行为(UB)。
现有尝试方案
尝试过多种实现,但没有一种能满足所有要求:性能最优的方案违反C++语言规则,合规方案性能不理想。理想的生成代码应像idx0和idx1那样,以this为基址、4*i为偏移量,通过基址偏移寻址(x64平台的lea指令)计算返回地址,但这两个实现都存在UB:
存在UB的高性能实现
struct vec3 { union { struct { float x; float y; float z; }; float xyz[3]; }; [[gnu::used]] constexpr float& idx0(size_t i) { // UB:违反严格别名规则 return reinterpret_cast<float*>(this)[i]; } [[gnu::used]] constexpr float& idx1(size_t i) { // UB:访问联合体的非活跃成员(xyz仅在此处使用) return xyz[i]; } };
Clang生成的汇编代码:
vec3::idx0(unsigned long): lea rax, [rdi + 4*rsi] ret vec3::idx1(unsigned long): lea rax, [rdi + 4*rsi] ret
合规但性能较差的实现
其他实现包括用if、switch手动分发,基于绝对/相对地址索引等,生成的汇编代码效率低很多:
struct vec3 { // 数据成员同上 [[gnu::used]] constexpr float& idx2(size_t i) { if (i == 0) { return x; } else if (i == 1) { return y; } else if (i == 2) { return z; } else { std::unreachable(); } } [[gnu::used]] constexpr float& idx3(size_t i) { switch (i) { case 0: return x; case 1: return y; case 2: return z; default: std::unreachable(); } } [[gnu::used]] constexpr float& idx4(size_t i) { auto addrs = std::array{&x, &y, &z}; return *addrs[i]; } [[gnu::used]] constexpr float& idx5(size_t i) { auto accessors = std::array{&vec3::x, &vec3::y, &vec3::z}; return this->*accessors[i]; } };
Clang生成的汇编代码:
vec3::idx2(unsigned long): mov rax, rdi test rsi, rsi je .LBB2_4 cmp rsi, 2 jne .LBB2_2 add rax, 8 .LBB2_4: ret .LBB2_2: add rax, 4 ret vec3::idx3(unsigned long): mov rax, rdi test rsi, rsi je .LBB3_4 cmp rsi, 2 jne .LBB3_2 add rax, 8 .LBB3_4: ret .LBB3_2: add rax, 4 ret vec3::idx4(unsigned long): mov qword ptr [rsp - 24], rdi lea rax, [rdi + 4] mov qword ptr [rsp - 16], rax add rdi, 8 mov qword ptr [rsp - 8], rdi mov rax, qword ptr [rsp + 8*rsi - 24] ret vec3::idx5(unsigned long): mov rax, rdi lea rcx, [rip + .L__const.vec3::idx5(unsigned long).accessors] add rax, qword ptr [rcx + 8*rsi] ret .L__const.vec3::idx5(unsigned long).accessors: .quad 0 .quad 4 .quad 8
尝试过通过[[assume]]或内置函数向编译器提供额外信息,帮助分析i与返回地址的数值关系,但均未奏效。
核心问题
- 是否存在一种标准、无UB的C++实现方式,同时能让编译器生成理想的
lea指令代码?可能只是没找到正确的编译器提示方式。 - 如果不存在,是因为语言标准的限制,还是编译器缺乏特定优化技术?比如
idx5的情况,编译器本应能识别出加载的偏移量始终等于4*i。
关于C++标准变更的疑问
C++标准的某些变更(或现有提案)能否改善这种情况?以下两种方案是否可行:
- 允许访问联合体的非活跃成员,前提是所有成员均为无填充的平凡可复制类型。
- 等待
std::start_lifetime_as实现,并用它来实现operator[](如idx6)。
内容的提问来源于stack exchange,提问作者Kirisame Igna
相关产品推荐
相关产品推荐

