了解微架构原因,使更长的代码执行速度提高 4 倍(AMD Zen 2 架构)

2024-03-03

我有以下 C++17 代码,是在 x64 模式下使用 VS 2019(版本 16.8.6)编译的:

struct __declspec(align(16)) Vec2f { float v[2]; };
struct __declspec(align(16)) Vec4f { float v[4]; };

static constexpr std::uint64_t N = 100'000'000ull;

const Vec2f p{};
Vec4f acc{};

// Using virtual method:
for (std::uint64_t i = 0; i < N; ++i)
    acc += foo->eval(p);

// Using function pointer:
for (std::uint64_t i = 0; i < N; ++i)
    acc += eval_fn(p);

在第一个循环中,foo is a std::shared_ptr and eval()是一个虚方法:

__declspec(noinline) virtual Vec4f eval(const Vec2f& p) const noexcept
{
    return { p.v[0], p.v[1], p.v[0], p.v[1] };
}

在第二个循环中,eval_fn是指向以下函数的指针:

__declspec(noinline) Vec4f eval_fn_impl(const Vec2f& p) noexcept
{
    return { p.v[0], p.v[1], p.v[0], p.v[1] };
}

最后,我有两个实现operator+= for Vec4f:

  • 一种使用显式循环实现:

    Vec4f& operator+=(Vec4f& lhs, const Vec4f& rhs) noexcept
    {
        for (std::uint32_t i = 0; i < 4; ++i)
            lhs.v[i] += rhs.v[i];
        return lhs;
    }
    
  • 还有一种是用 SSE 内在函数实现的:

    Vec4f& operator+=(Vec4f& lhs, const Vec4f& rhs) noexcept
    {
        _mm_store_ps(lhs.v, _mm_add_ps(_mm_load_ps(lhs.v), _mm_load_ps(rhs.v)));
        return lhs;
    }
    

您可以在下面找到测试的完整(独立、仅限 Windows)代码。

以下是两个循环的生成代码,以及在AMD 线程撕裂者 3970XCPU(Zen 2架构):

  • 随着上证内在实施operator+=(Vec4f&, const Vec4f&):

    // Using virtual method: 649 ms
    $LL4@main:
      mov rax, QWORD PTR [rdi]            // fetch vtable base pointer (rdi = foo)
      lea r8, QWORD PTR p$[rsp]           // r8 = &p
      lea rdx, QWORD PTR $T3[rsp]         // not sure what $T3 is (some kind of temporary, but why?)
      mov rcx, rdi                        // rcx = this
      call    QWORD PTR [rax]             // foo->eval(p)
      addps   xmm6, XMMWORD PTR [rax]
      sub rbp, 1
      jne SHORT $LL4@main
    
    // Using function pointer: 602 ms
    $LL7@main:
      lea rdx, QWORD PTR p$[rsp]          // rdx = &p
      lea rcx, QWORD PTR $T2[rsp]         // same question as above
      call    rbx                         // eval_fn(p)
      addps   xmm6, XMMWORD PTR [rax]
      sub rsi, 1
      jne SHORT $LL7@main
    
  • 随着显式循环实施operator+=(Vec4f&, const Vec4f&):

    // Using virtual method: 167 ms [3.5x to 4x FASTER!]
    $LL4@main:
      mov rax, QWORD PTR [rdi]
      lea r8, QWORD PTR p$[rsp]
      lea rdx, QWORD PTR $T5[rsp]
      mov rcx, rdi
      call    QWORD PTR [rax]
      addss   xmm9, DWORD PTR [rax]
      addss   xmm8, DWORD PTR [rax+4]
      addss   xmm7, DWORD PTR [rax+8]
      addss   xmm6, DWORD PTR [rax+12]
      sub rbp, 1
      jne SHORT $LL4@main
    
    // Using function pointer: 600 ms
    $LL7@main:
      lea rdx, QWORD PTR p$[rsp]
      lea rcx, QWORD PTR $T4[rsp]
      call    rbx
      addps   xmm6, XMMWORD PTR [rax]
      sub rsi, 1
      jne SHORT $LL7@main
    

(据我所知,在 AMD Zen 2 架构上,addss and addps指令有 3 个周期的延迟,最多可以同时执行两条这样的指令。)

让我困惑的情况是使用虚拟方法和显式循环实现时operator+=:

为什么它比其他三个变体快 3.5 倍到 4 倍?

这里有哪些相关的建筑效果?循环后续迭代中寄存器之间的依赖性更少?或者缓存方面运气不好?


完整源代码:

#include <Windows.h>
#include <cstdint>
#include <cstdio>
#include <memory>
#include <xmmintrin.h>

struct __declspec(align(16)) Vec2f
{
    float v[2];
};

struct __declspec(align(16)) Vec4f
{
    float v[4];
};

Vec4f& operator+=(Vec4f& lhs, const Vec4f& rhs) noexcept
{
#if 0
    _mm_store_ps(lhs.v, _mm_add_ps(_mm_load_ps(lhs.v), _mm_load_ps(rhs.v)));
#else
    for (std::uint32_t i = 0; i < 4; ++i)
        lhs.v[i] += rhs.v[i];
#endif
    return lhs;
}

std::uint64_t get_timer_freq()
{
    LARGE_INTEGER frequency;
    QueryPerformanceFrequency(&frequency);
    return static_cast<std::uint64_t>(frequency.QuadPart);
}

std::uint64_t read_timer()
{
    LARGE_INTEGER count;
    QueryPerformanceCounter(&count);
    return static_cast<std::uint64_t>(count.QuadPart);
}

struct Foo
{
    __declspec(noinline) virtual Vec4f eval(const Vec2f& p) const noexcept
    {
        return { p.v[0], p.v[1], p.v[0], p.v[1] };
    }
};

using SampleFn = Vec4f (*)(const Vec2f&);

__declspec(noinline) Vec4f eval_fn_impl(const Vec2f& p) noexcept
{
    return { p.v[0], p.v[1], p.v[0], p.v[1] };
}

__declspec(noinline) SampleFn make_eval_fn()
{
    return &eval_fn_impl;
}

int main()
{
    static constexpr std::uint64_t N = 100'000'000ull;

    const auto timer_freq = get_timer_freq();
    const Vec2f p{};
    Vec4f acc{};

    {
        const auto foo = std::make_shared<Foo>();
        const auto start_time = read_timer();
        for (std::uint64_t i = 0; i < N; ++i)
            acc += foo->eval(p);
        std::printf("foo->eval: %llu ms\n", 1000 * (read_timer() - start_time) / timer_freq);
    }

    {
        const auto eval_fn = make_eval_fn();
        const auto start_time = read_timer();
        for (std::uint64_t i = 0; i < N; ++i)
            acc += eval_fn(p);
        std::printf("eval_fn: %llu ms\n", 1000 * (read_timer() - start_time) / timer_freq);
    }

    return acc.v[0] + acc.v[1] + acc.v[2] + acc.v[3] > 0.0f ? 1 : 0;
}

我正在 Intel Haswell 处理器上对此进行测试,但性能结果相似,我猜原因也相似,但请对此持保留态度。 Haswell 和 Zen 2 之间当然存在差异,但据我所知,我所指责的效果应该适用于它们。

问题是:虚拟方法/通过指针调用的函数/无论它是什么,都会存储 4 个标量,但随后主循环会对同一内存进行向量加载。存储到加载转发可以处理存储值然后立即加载的各种情况,但通常不会处理这样的情况:加载依赖于多个存储(更一般地说:依赖于仅部分提供的存储的加载)负载尝试加载的数据)。假设这是可能的,但这不是当前微架构的特征。

作为实验,更改虚拟方法中的代码以使用向量存储。例如:

__declspec(noinline) virtual Vec4f eval(const Vec2f& p) const noexcept
{
    Vec4f r;
    auto pv = _mm_load_ps(p.v);
    _mm_store_ps(r.v, _mm_shuffle_ps(pv, pv, _MM_SHUFFLE(1, 0, 1, 0)));
    return r;
}

在我的 PC 上,这使时间与快速版本保持一致,这支持了以下假设:问题是由多个标量存储馈入矢量负载引起的。

从 8 字节加载 16 字节Vec2f并不完全合法,如有必要可以解决。只有 SSE(1) 有点烦人,SSE3 会很好_mm_loaddup_pd (aka movddup).

如果 MSVC 返回了Vec4f结果通过寄存器而不是通过外指针,但我不知道如何说服它这样做,除了将返回类型更改为__m128. __vectorcall也有帮助,但使 MSVC 返回结构几个寄存器然后在调用者中通过额外的洗牌重新组合。它有点混乱,比任何一个快速选项都慢,但仍然比存储转发失败的版本快。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

了解微架构原因,使更长的代码执行速度提高 4 倍(AMD Zen 2 架构) 的相关文章

随机推荐

  • 设计在不同的屏幕上破碎[关闭]

    Closed 这个问题需要多问focused help closed questions 目前不接受答案 我设计了一个网页 其中很少有HTML元素没有响应 这些元素是动态生成的jQuery我相信 应该有一种方法可以让他们看起来反应灵敏 Me
  • 如何针对 TextReader 使用正则表达式?

    在 可能 非常大的文本中查找模式的最佳方法是什么 我可以使用正则表达式 但它接受字符串作为参数 有没有办法将它与 TextReader 或某种流一起使用 不 正则表达式可能需要进行回溯 由于流只能向前读取 这意味着无论如何它都必须将整个流保
  • 无法在 Docker 容器内从 Google API 交换 AccessToken

    我有一个用 Go 编写的网络应用程序 使用 oauth2 包golang org x oauth2 通过 Google 登录用户 按照本教程操作https developers google com identity sign in web
  • Facebook API - uids 和电子邮件 [关闭]

    Closed 这个问题需要多问focused help closed questions 目前不接受答案 我正在尝试开发一个应用程序 用户 X 已登录 Facebook 应用程序有一组电子邮件地址 可能是用户 X 的朋友 应用程序获取用户
  • 为什么要在 Objective C 构造函数中使用 [super init]?

    假设我有一个名为 Item 的类 它是 NewsItem 和 TwitterItem 的超类 如果我想创建一些 NewsItem 我必须使用 在构造函数内 self super init 如果是 为什么 在 Java C 中我会这样做 Ne
  • Flexbox 实现水平滚动

    我正在使用 Flexbox 布局制作一个购物中心网站 但 Flexbox 在某些区域会导致问题 Flexbox 似乎在某些区域进行水平滚动 但我不希望这样 您的问题是横幅中的图像太宽 无法适应大多数视口 因此它们迫使页面变宽 没有简单的方法
  • 在 R 中重复多个 NULL

    在我的模拟中 我需要一个如下所示的向量 vec NULL NULL NULL NULL 2 2 2 2 4 4 4 4 但是 在 R 中 当我使用rep NULL 4 它什么也不返回 例如 vec all c rep NULL 4 rep
  • SwiftUI 垂直未对齐的文本

    我有一些垂直未对齐的文本 我不知道为什么 代码 struct IBANInputView View State var securityDigits State var bankCode State var accountNumber va
  • CSS 动画在 Chrome 中不起作用

    对项目的一些见解 一个完整的交互式网站 动画将在其中播放 然后它们会停止 一旦它们停止并且动画上发生鼠标悬停事件 它将播放直到动画完成 70 当单击另一个对象时 在本例中 案例文本 它将完成它的循环 我的项目合作伙伴在 Google Web
  • 语法与运算符结合性之间的关系

    一些编译器书籍 文章 论文谈论语法的设计及其运算符的结合性的关系 我是自上而下的忠实粉丝 尤其是递归下降 解析器 到目前为止我编写的大多数 如果不是全部 编译器都使用以下表达式语法 Expr Term Term Term Factor Fa
  • 适用于 emacs 的交互式拼写检查程序

    我最近切换到 emacs24 并升级到 Ubuntu 12 04 我似乎无法让 hunspell 再次正常工作 我发现 hunspell 总体上比 aspell 更好 Hunspell 只想使用澳大利亚词典而不是英语美国词典 否则会冻结我的
  • 如何将 selenium webelelements 转换为 python 中的字符串列表

    我从 Scopus 网站收集了强制性数据 我的输出已保存在名为 文档 的列表中 当我对这个列表的每个元素使用 type 方法时 python 返回这个类
  • 接受 Flask url 中的整数列表而不是一个整数

    我的 API 有一个通过 url 中传递的 int id 来处理用户的路由 我想传递一个 id 列表 这样我就可以向 API 发出一个批量请求 而不是多个单个请求 我如何接受 id 列表 app route user
  • 运行应用程序时,在 VSCode 中启动 ASP.NET Core 应用程序中的特定 URL

    如果重要的话 可以在 Mac 上运行 在 Visual Studio 中 您可以在运行或调试 ASP NET Core 项目时使用此方法启动特定 URL 使用 launchUrl 属性 这是一个示例 launchSettings json
  • Laravel 按日期时间格式中的小时进行分组

    如何按一天中的时间对网站上的 注册 进行分组 我已经尝试过了 但它不起作用 regs DB table registrations gt select createddatetime DB raw COUNT id gt groupBy D
  • 如何访问 docker-compose 在 haproxy 配置中创建的副本

    我有一个简单的 haproxy cfg 如下所示 frontend http bind 8080 mode http use backend all backend all mode http server s1 ws 8080 现在我有一
  • 在 Haskell 中查找给定集合 A 和通用集合 U 的补集 [关闭]

    Closed 这个问题需要多问focused help closed questions 目前不接受答案 我尝试编写一个函数 补集 给定一个集合 A 和一个通用集 U 返回 A 相对于 U 的补集 包装在 Just 类型构造函数中 我必须仔
  • 根据标题开始位置将 CSV 读入 R

    我有大量 CSV 文件 有些标题从第一行开始 有些标题从第三行开始 有些则从第七行开始 依此类推 标题看起来都一样 只是在不同文件的不同行上开始 有没有办法有条件地读取 csv 文件以从标题开始的位置开始 例如 如果我知道标题都具有第一个列
  • 在附加的堆栈跟踪中获取了资源但从未释放 - 错误

    我不确定为什么会收到此错误 五分之二的我使用模拟器设置壁纸时 收到错误 在附加的堆栈跟踪中获取了资源 但从未释放 有关避免资源泄漏的信息 请参阅 java io Closeable 当我使用手机设置壁纸时 效果非常好 没有任何故障 为什么我
  • 了解微架构原因,使更长的代码执行速度提高 4 倍(AMD Zen 2 架构)

    我有以下 C 17 代码 是在 x64 模式下使用 VS 2019 版本 16 8 6 编译的 struct declspec align 16 Vec2f float v 2 struct declspec align 16 Vec4f