与普通 POD 类型相比,如何使用仅包含一个 POD 成员的简单类来克服性能下降?

问题描述

我真的不知道问题的根源是否真的与 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) 实现类似的性能

现在我想到了两个问题:

  1. 变体 (C) 在标准方面是否合法,还是实际上是 UB 或类似的东西。
  2. 有什么方法可以定义模板类 Pod,使其在性能方面的行为类似于基础值类型。

如果有帮助,我可以使用 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)之外别无他法。