gpt4 book ai didi

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

转载 作者:行者123 更新时间:2023-12-04 03:33:40 31 4
gpt4 key购买 nike

我真的不知道问题的根源是否真的与 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/

31 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com