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

标签 c++ performance templates visual-c++ c++17

我真的不知道问题的根源是否真的与 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, typename TValue>
Pod<TTag, TValue> operator*( Pod<TTag, TValue> lhs, 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>> lhs( size );
std::vector<Pod<A_tag, float>> rhs( size );
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 ];
}

我可以为 (C) 实现与 (B) 类似的性能。

现在我想到了两个问题:

  1. 关于标准,变体 (C) 是否合法,或者实际上是 UB 或类似的东西。
  2. 有没有办法定义模板类 Pod,使其在性能方面与底层值类型相似。

如果有帮助,我可以使用 C++17。我使用 Visual Studio 2019 Release 模式/O2 测试了所有内容。

感谢您的帮助!

最佳答案

Is variant (C) legal regarding standard or is it actually UB or something similar.

变体 (C) 在技术上是 UB - 对任何 i 的严格别名违规>= 1. 您正在遍历指向 float 的指针住在里面Pod就好像它本身就在一个数组中一样。但是Pod<...>float是不兼容的类型。

此外,它不可移植 - 在 VC++ 中可能没问题,但技术上可以在 Pod 末尾进行填充,所以不能保证 float s 将被排列成没有间隙。

Is there any way to define the template class Pod that it behaves similar to the underlying value type in regards of performance.

是的,您可以在第一个版本中禁用自动矢量化,然后所有版本的性能都相似:)

#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], xmm0
        movss   xmm1, 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, DWORD PTR [r9+rax-12]
        mov     rax, QWORD PTR [r8]
        movss   DWORD PTR [r9+rax-12], xmm0
        mov     rax, QWORD PTR [rdx]
        movss   xmm0, DWORD PTR [r9+rax-8]
        mov     rax, QWORD PTR [rcx]
        addss   xmm0, DWORD PTR [r9+rax-8]
        mov     rax, QWORD PTR [r8]
        movss   DWORD PTR [r9+rax-8], xmm0
        mov     rax, QWORD PTR [rdx]
        movss   xmm0, DWORD PTR [r9+rax-4]
        mov     rax, QWORD PTR [rcx]
        addss   xmm0, DWORD PTR [r9+rax-4]
        mov     rax, QWORD PTR [r8]
        movss   DWORD PTR [r9+rax-4], xmm0
        mov     rax, QWORD PTR [rdx]
        movss   xmm0, DWORD PTR [r9+rax]
        mov     rax, QWORD PTR [rcx]
        addss   xmm0, DWORD PTR [r9+rax]
        mov     rax, QWORD PTR [r8]
        movss   DWORD PTR [r9+rax], xmm0
        mov     rax, QWORD PTR [rdx]
        movss   xmm0, DWORD PTR [r9+rax+4]
        mov     rax, QWORD PTR [rcx]
        addss   xmm0, DWORD PTR [r9+rax+4]
        mov     rax, QWORD PTR [r8]
        movss   DWORD PTR [r9+rax+4], xmm0
        mov     rax, QWORD PTR [rdx]
        movss   xmm0, DWORD PTR [r9+rax+8]
        mov     rax, QWORD PTR [rcx]
        addss   xmm0, DWORD PTR [r9+rax+8]
        mov     rax, QWORD PTR [r8]
        movss   DWORD PTR [r9+rax+8], xmm0
        mov     rax, QWORD PTR [rdx]
        movss   xmm0, DWORD PTR [r9+rax+12]
        mov     rax, QWORD PTR [rcx]
        addss   xmm0, DWORD PTR [r9+rax+12]
        mov     rax, QWORD PTR [r8]
        movss   DWORD PTR [r9+rax+12], xmm0
        mov     rax, QWORD PTR [rdx]
        movss   xmm0, DWORD PTR [r9+rax+16]
        mov     rax, QWORD PTR [rcx]
        addss   xmm0, DWORD PTR [r9+rax+16]
        mov     rax, QWORD PTR [r8]
        movss   DWORD PTR [r9+rax+16], xmm0
        mov     rax, QWORD PTR [rdx]
        movss   xmm0, DWORD PTR [r9+rax+20]
        mov     rax, QWORD PTR [rcx]
        addss   xmm0, DWORD PTR [r9+rax+20]
        mov     rax, QWORD PTR [r8]
        movss   DWORD PTR [r9+rax+20], xmm0
        mov     rax, QWORD PTR [rdx]
        movss   xmm0, DWORD PTR [r9+rax+24]
        mov     rax, QWORD PTR [rcx]
        addss   xmm0, 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), rhs(size), res(size);
    for (auto _ : state) {
        PodVec(lhs, rhs, res);
        benchmark::DoNotOptimize(res);
    }
}
BENCHMARK(PodVecTest);

void PodPtrTest(benchmark::State& state) {
    std::vector<Pod<float>> lhs(size), rhs(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), rhs(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> 别无他法。 s(或切换到 GCC)。

关于c++ - 与普通 POD 类型相比,如何克服仅包含一个 POD 成员的简单类的性能下降?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/67359445/

相关文章:

visual-studio - VS 2008,分析多个测试

c++ - C++ 中的变量包

c++ - 不断收到错误 LNK2019 : unresolved external symbol

c++ - PCL 在没有云复制的情况下对八叉树应用过滤器

Java调用类型性能

c++ - 模板警告和错误帮助,(gcc)

c++ - 是什么导致 SXSTrace 错误 "StartTrace failed ... Unknown Error"?

java - antlr4 在多核 CPU 上的性能

php - 带有 str_replace 的 HTML 电子邮件模板

c++ - 奇怪的行为可变参数模板