问题描述
我真的不知道问题的根源是否真的与 POD 类型有关,但至少我可以看到其中的区别。我想要实现的是,我的模板 Pod
类在性能方面表现得像一个简单的内置类型(例如 float)。
template<typename TTag,typename TValue>
class Pod
{
public:
Pod() = default;
Pod( TValue value ) : m_value{ value } {};
TValue& value() noexcept
{
return m_value;
}
TValue const& value() const noexcept
{
return m_value;
}
Pod& operator+=( Pod const& src ) noexcept
{
m_value += src.m_value;
return *this;
}
Pod& operator*=( TValue const& src ) noexcept
{
m_value *= src;
return *this;
}
private:
TValue m_value{};
};
template<typename TTag,typename TValue>
Pod<TTag,TValue>
operator+( Pod<TTag,TValue> lhs,Pod<TTag,TValue> const& rhs ) noexcept
{
lhs += rhs;
return lhs;
}
template<typename TTag,TValue> operator*( Pod<TTag,TValue const& rhs ) noexcept
{
lhs *= rhs;
return lhs;
}
现在让我们进行一个简单的测试操作,例如:(A)
std::vector<Pod<A_tag,float>> lhs( size );
std::vector<Pod<A_tag,float>> rhs( size );
std::vector<Pod<A_tag,float>> res( size );
for ( std::size_t i = 0; i < size; ++i )
{
res[ i ] = lhs[ i ] + rhs[ i ];
}
并将其与:(B)
std::vector<float> lhs( size );
std::vector<float> rhs( size );
std::vector<float> res( size );
for ( std::size_t i = 0; i < size; ++i )
{
res[ i ] = lhs[ i ] + rhs[ i ];
}
我在 googlebenchmark
测试用例中注意到,(B) 比 (A) 快大约 3 倍。
如果我将(A)改为:(C)
std::vector<Pod<A_tag,float>> res( size );
auto plhs = &( lhs[ 0 ].value() );
auto prhs = &( rhs[ 0 ].value() );
for ( std::size_t i = 0; i < size; ++i )
{
res[ i ].value() = plhs[ i ] + prhs[ i ];
}
我可以像 (B) 一样为 (C) 实现类似的性能。
现在我想到了两个问题:
如果有帮助,我可以使用 C++17。我使用 Visual Studio 2019 发布 模式 /O2 测试了所有内容。
感谢您的帮助!
解决方法
关于标准的变体 (C) 是否合法,或者它实际上是 UB 或类似的东西。
变体 (C) 在技术上是 UB - 任何 i
>= 1 的严格别名违规。您正在迭代指向位于 float
内的 Pod
指针,就好像它一样本身就在一个数组中。但是 Pod<...>
和 float
是不兼容的类型。
此外它不可移植——在 VC++ 中可能没问题,但技术上可以在 Pod
的末尾填充,所以不能保证 float
的排列没有间隙他们之间。
是否有任何方法可以定义模板类 Pod
,使其在性能方面的行为类似于基础值类型。
是的,您可以在第一个版本中禁用自动矢量化,然后所有版本都会执行类似的操作:)
#pragma loop( no_vector )
基本上 MSVC 目前只能自动矢量化非常简单的循环。我们可以看到 here 它自动矢量化了第一个版本(如果我们可以这样称呼它):
$LL25@Baseline:
movss xmm0,DWORD PTR [rax+r9*4]
addss xmm0,DWORD PTR [r10+r9*4]
movss DWORD PTR [rdx+r9*4],xmm0
movss xmm1,DWORD PTR [r10+r9*4+4]
addss xmm1,DWORD PTR [rax+r9*4+4]
movss DWORD PTR [rdx+r9*4+4],xmm1
movss xmm0,DWORD PTR [rax+r9*4+8]
addss xmm0,DWORD PTR [r10+r9*4+8]
movss DWORD PTR [rdx+r9*4+8],DWORD PTR [rax+r9*4+12]
addss xmm1,DWORD PTR [r10+r9*4+12]
movss DWORD PTR [rdx+r9*4+12],xmm1
add r9,4
cmp r9,9997 ; 0000270dH
jb SHORT $LL25@Baseline
cmp r9,10000 ; 00002710H
jae $LN23@Baseline
但不幸的是,它只是在 Pod
版本上发疯:
$LL4@PodVec:
mov rax,QWORD PTR [rdx]
movss xmm0,DWORD PTR [r9+rax-12]
mov rax,QWORD PTR [rcx]
addss xmm0,QWORD PTR [r8]
movss DWORD PTR [r9+rax-12],xmm0
mov rax,DWORD PTR [r9+rax-8]
mov rax,QWORD PTR [r8]
movss DWORD PTR [r9+rax-8],DWORD PTR [r9+rax-4]
mov rax,QWORD PTR [r8]
movss DWORD PTR [r9+rax-4],DWORD PTR [r9+rax]
mov rax,QWORD PTR [r8]
movss DWORD PTR [r9+rax],DWORD PTR [r9+rax+4]
mov rax,QWORD PTR [r8]
movss DWORD PTR [r9+rax+4],DWORD PTR [r9+rax+8]
mov rax,QWORD PTR [r8]
movss DWORD PTR [r9+rax+8],DWORD PTR [r9+rax+12]
mov rax,QWORD PTR [r8]
movss DWORD PTR [r9+rax+12],DWORD PTR [r9+rax+16]
mov rax,QWORD PTR [r8]
movss DWORD PTR [r9+rax+16],DWORD PTR [r9+rax+20]
mov rax,QWORD PTR [r8]
movss DWORD PTR [r9+rax+20],DWORD PTR [r9+rax+24]
mov rax,QWORD PTR [r8]
movss DWORD PTR [r9+rax+24],xmm0
add r9,40 ; 00000028H
cmp r9,40012 ; 00009c4cH
jb $LL4@PodVec
我尝试通过各种合法方式帮助它,通过简化代码使其最容易优化:
#include <vector>
#include <benchmark/benchmark.h>
template<typename TValue>
struct Pod {
public:
Pod() noexcept {}
Pod(TValue value) noexcept : m_value{ value } {}
Pod operator+ (Pod src) const noexcept {
return m_value + src.m_value;
}
private:
TValue m_value{};
};
constexpr std::size_t size = 10000;
inline void Baseline(std::vector<float>& lhs,std::vector<float>& rhs,std::vector<float>& res) {
for (std::size_t i = 0; i < size; ++i)
{
res[i] = lhs[i] + rhs[i];
}
}
inline void PodVec(std::vector<Pod<float>>& lhs,std::vector<Pod<float>>& rhs,std::vector<Pod<float>>& res) {
for (std::size_t i = 0; i < size; ++i)
{
res[i] = lhs[i] + rhs[i];
}
}
inline void PodPtr(Pod<float>* lhs,Pod<float>* rhs,Pod<float>* res) {
for (std::size_t i = 0; i < size; ++i)
{
res[i] = lhs[i] + rhs[i];
}
}
using Iter = std::vector<Pod<float>>::iterator;
inline void PodIter(Iter lhs,Iter rhs,Iter res,Iter endres) {
for (; res != endres; lhs++,rhs++,res++)
{
*res = *lhs + *rhs;
}
}
void BaselineTest(benchmark::State& state) {
std::vector<float> lhs(size),rhs(size),res(size);
for (auto _ : state) {
Baseline(lhs,rhs,res);
benchmark::DoNotOptimize(res);
}
}
BENCHMARK(BaselineTest);
void PodVecTest(benchmark::State& state) {
std::vector<Pod<float>> lhs(size),res(size);
for (auto _ : state) {
PodVec(lhs,res);
benchmark::DoNotOptimize(res);
}
}
BENCHMARK(PodVecTest);
void PodPtrTest(benchmark::State& state) {
std::vector<Pod<float>> lhs(size),res(size);
for (auto _ : state) {
PodPtr(&lhs[0],&rhs[0],&res[0]);
benchmark::DoNotOptimize(res);
}
}
BENCHMARK(PodPtrTest);
void PodIterTest(benchmark::State& state) {
std::vector<Pod<float>> lhs(size),res(size);
for (auto _ : state) {
PodIter(begin(lhs),begin(rhs),begin(res),end(res));
benchmark::DoNotOptimize(res);
}
}
BENCHMARK(PodIterTest);
但不幸的是,不幸的是(但奇怪的是,每个版本的表现都不同):
-------------------------------------------------------
Benchmark Time CPU Iterations
-------------------------------------------------------
BaselineTest 1824 ns 1842 ns 373333
PodVecTest 6166 ns 6278 ns 112000
PodPtrTest 5185 ns 5162 ns 112000
PodIterTest 4663 ns 4604 ns 149333
同时,GCC 10 任何一个版本都没有问题:
----------------------------------------------------
Benchmark Time CPU Iterations
----------------------------------------------------
BaselineTest 1469 ns 1469 ns 422409
PodVecTest 1464 ns 1464 ns 484971
PodPtrTest 1424 ns 1424 ns 491200
PodIterTest 1420 ns 1420 ns 495973
这就是 GCC 循环程序集的样子:
.L6:
movss xmm0,DWORD PTR [rcx+rax*4]
addss xmm0,DWORD PTR [rsi+rax*4]
movss DWORD PTR [rdx+rax*4],xmm0
add rax,1
cmp rax,10000
jne .L6
有趣的是,Clang 11 在 vector<Pod>
版本中也遇到了一些困难(link),这是出乎意料的。
故事的寓意:there are no zero-cost abstractions。在此示例中,如果您想从矢量化中受益,我认为除了使用“原始”vector<float>
(或切换到 GCC)之外别无他法。