为什么在强度降低乘法和循环进位加法之后,这段代码的执行速度会变慢?

2024-05-09

我正在读书阿格纳·雾 https://en.wikipedia.org/wiki/Agner_Fog's 优化手册 https://en.wikipedia.org/wiki/Agner_Fog#Optimization,我遇到了这个例子:

double data[LEN];

void compute()
{
    const double A = 1.1, B = 2.2, C = 3.3;

    int i;
    for(i=0; i<LEN; i++) {
        data[i] = A*i*i + B*i + C;
    }
}

Agner 指出,有一种方法可以优化此代码 - 通过认识到循环可以避免使用昂贵的乘法,而是使用每次迭代应用的“增量”。

我用一张纸来证实这个理论,首先......

...当然,他是对的 - 在每次循环迭代中,我们可以通过添加“增量”,基于旧结果计算新结果。该增量从值“A+B”开始,然后每一步增加“2*A”。

所以我们将代码更新为如下所示:

void compute()
{
    const double A = 1.1, B = 2.2, C = 3.3;
    const double A2 = A+A;
    double Z = A+B;
    double Y = C;

    int i;
    for(i=0; i<LEN; i++) {
        data[i] = Y;
        Y += Z;
        Z += A2;
    }
}

就操作复杂性而言,这两个版本的功能之间的差异确实是惊人的。与加法相比,我们的 CPU 中的乘法运算速度要慢得多。我们用 2 个加法替换了 3 个乘法和 2 个加法……

所以我继续添加一个循环来执行compute很多次 - 然后保留执行所需的最短时间:

unsigned long long ts2ns(const struct timespec *ts)
{
    return ts->tv_sec * 1e9 + ts->tv_nsec;
}

int main(int argc, char *argv[])
{
    unsigned long long mini = 1e9;
    for (int i=0; i<1000; i++) {
        struct timespec t1, t2;
        clock_gettime(CLOCK_MONOTONIC_RAW, &t1);
        compute();
        clock_gettime(CLOCK_MONOTONIC_RAW, &t2);
        unsigned long long diff = ts2ns(&t2) - ts2ns(&t1);
        if (mini > diff) mini = diff;
    }
    printf("[-] Took: %lld ns.\n", mini);
}

我编译了两个版本,运行它们......然后看到这个:

gcc -O3 -o 1 ./code1.c

gcc -O3 -o 2 ./code2.c

./1

[-] Took: 405858 ns.

./2

[-] Took: 791652 ns.

嗯,这是出乎意料的。由于我们报告了最短执行时间,因此我们丢弃了操作系统各个部分引起的“噪音”。我们还小心翼翼地在一台完全不执行任何操作的机器上运行。结果或多或少是可重复的 - 重新运行两个二进制文件表明这是一致的结果:

for i in {1..10} ; do ./1 ; done

[-] Took: 406886 ns.
[-] Took: 413798 ns.
[-] Took: 405856 ns.
[-] Took: 405848 ns.
[-] Took: 406839 ns.
[-] Took: 405841 ns.
[-] Took: 405853 ns.
[-] Took: 405844 ns.
[-] Took: 405837 ns.
[-] Took: 406854 ns.

for i in {1..10} ; do ./2 ; done

[-] Took: 791797 ns.
[-] Took: 791643 ns.
[-] Took: 791640 ns.
[-] Took: 791636 ns.
[-] Took: 791631 ns.
[-] Took: 791642 ns.
[-] Took: 791642 ns.
[-] Took: 791640 ns.
[-] Took: 791647 ns.
[-] Took: 791639 ns.

接下来要做的唯一一件事是查看编译器为两个版本中的每一个创建了什么样的代码。

objdump -d -S表明第一个版本compute- “愚蠢”但不知何故快速的代码 - 有一个如下所示的循环:

那么第二个优化版本呢?它只添加了两个内容?

现在我不了解你的情况,但就我自己而言,我……很困惑。第二个版本的指令大约减少了 4 倍,其中两个主要指令只是SSE https://en.wikipedia.org/wiki/Streaming_SIMD_Extensions基于添加(addsd)。第一个版本不仅有 4 倍多的指令...它还包含完整的(如预期的)乘法(mulpd).

我承认我没想到这个结果。不是因为我是阿格纳的粉丝(我是,但这无关紧要)。

知道我缺少什么吗?我在这里犯了任何错误吗?这可以解释速度的差异吗?请注意,我已经完成了测试至强 W5580 https://en.wikipedia.org/wiki/List_of_Intel_Xeon_processors_(Nehalem-based)#%22Gainestown%22_(45_nm) and a 至强E5-1620 https://en.wikipedia.org/wiki/List_of_Intel_Xeon_processors_(Sandy_Bridge-based)#Xeon_E5-1620- 在这两个版本中,第一个(哑)版本比第二个版本快得多。

为了轻松重现结果,两个版本的代码有两个要点:愚蠢但不知何故更快 https://gist.github.com/ttsiodras/7b4f4d6948886500cd8fa32fc90dab41 and 优化,但速度较慢 https://gist.github.com/ttsiodras/178ddc5dd696948a1b34bea09500d96a.

附:请不要评论浮点精度问题;这不是这个问题的重点。


了解您所看到的性能差异的关键在于矢量化。是的,基于加法的解决方案在其内部循环中只有两条指令,但重要的区别不在于how many循环中有指令,但是在多少工作量每条指令正在执行。

在第一个版本中,输出完全依赖于输入:每个data[i]是一个函数i本身,这意味着每个data[i]可以按任何顺序计算:编译器可以向前、向后、横向等等进行计算,并且您仍然会得到相同的结果 - 除非您从另一个线程观察该内存,否则您永远不会注意到数据的方向正在被挤压。

在第二个版本中,输出不依赖于i——这取决于A and Z从上次循环开始。

如果我们将这些循环的主体表示为小数学函数,它们将具有非常不同的整体形式:

  • f(i) -> di
  • f(Y, Z) -> (di, Y', Z')

在后一种形式中,没有实际依赖i— 计算函数值的唯一方法是了解前一个函数Y and Z从该函数的最后一次调用开始,这意味着这些函数形成一个链 - 在完成前一个函数之前,您无法执行下一个函数。

为什么这很重要?因为CPU有矢量平行指示each可以同时执行两个、四个甚至八个算术运算! (AVX https://en.wikipedia.org/wiki/Advanced_Vector_ExtensionsCPU 可以并行执行更多操作。)即四次乘法、四次加法、四次减法、四次比较 — 四个随便什么!因此,如果您尝试计算的输出是only取决于输入,那么您可以安全地一次执行两个、四个甚至八个 - 无论它们是向前还是向后都没关系,因为结果是相同的。但如果输出依赖于先前的计算,那么你就只能以串行的形式进行——一次一个。

这就是为什么“较长”的代码在性能上获胜的原因。尽管它有更多的设置,但实际上doing还有很多工作,大部分工作都是并行完成的:这不仅仅是计算data[i]在循环的每次迭代中——它都在计算data[i], data[i+1], data[i+2], and data[i+3]同时,然后跳到下一组四。

为了扩展一下我在这里的意思,编译器首先将原始代码转换为如下所示:

int i;
for (i = 0; i < LEN; i += 4) {
    data[i+0] = A*(i+0)*(i+0) + B*(i+0) + C;
    data[i+1] = A*(i+1)*(i+1) + B*(i+1) + C;
    data[i+2] = A*(i+2)*(i+2) + B*(i+2) + C;
    data[i+3] = A*(i+3)*(i+3) + B*(i+3) + C;
}

如果你眯着眼睛看的话,你可以说服自己,它会做与原版相同的事情。它之所以能做到这一点,是因为所有这些相同的垂直操作符:所有这些* and +操作是相同的操作,只是在不同的数据上执行 - 并且 CPU 有特殊的内置指令,可以执行多个操作*或多个+同时对不同的数据进行操作,每个操作仅在一个时钟周期内。

注意这封信p在更快的解决方案的说明中 -addpd and mulpd——还有那封信s在较慢的解决方案的说明中 -addsd。这就是“添加压缩双精度数”和“乘以压缩双精度数”与“添加单双精度数”。

不仅如此,编译器似乎也部分展开了循环——循环不仅仅执行two值每次迭代,但实际上four,并交错操作以避免依赖性和停顿,所有这些都减少了汇编代码必须测试的次数i < 1000以及。

然而,所有这一切只有在存在的情况下才有效没有依赖关系循环迭代之间:如果唯一决定每次发生的事情data[i] is i本身。如果存在依赖关系,如果上一次迭代的数据影响下一次迭代,那么编译器可能会受到它们的限制,以至于根本无法更改代码 - 而不是编译器能够使用花哨的并行指令或巧妙的优化(CSE https://en.wikipedia.org/wiki/Common_subexpression_elimination, 强度降低 https://en.wikipedia.org/wiki/Strength_reduction, 循环展开 https://en.wikipedia.org/wiki/Loop_unrolling、重新排序等),您将得到与您输入的代码完全相同的代码 - 添加 Y,然后添加 Z,然后重复。

但在这里,在代码的第一个版本中,编译器正确地认识到数据中没有依赖性,并发现它可以并行地完成工作,而且它确实做到了,这就是所有差异的原因。

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

为什么在强度降低乘法和循环进位加法之后,这段代码的执行速度会变慢? 的相关文章

  • Polygot 包含 nasm/yasm 和 C 的文件

    我有一堆幻数 我想将它们包含在由 nasm 或 yasm 编译的 C 程序和汇编文件中 在纯 C 语言中 该文件看起来像是一系列定义 例如 define BLESS 55378008 define ANSWER 42 在 nasm 或 ya
  • 从 DX:AX 寄存器转移到单个 32 位寄存器

    我在添加 16 位乘法的乘积时遇到问题 我想将一年 例如 2015 年 乘以 365 为此 我 mov dx 0 to clear the register mov ax cx cx holds the year such as 2015
  • 如何减少 MinGW g++ 编译器生成的可执行文件的大小?

    我有一个简单的 Hello world C 程序 在 Win XP 下由 MinGW g 编译器编译为 500kB 可执行文件 有人说这是由于iostream的库和静态链接libstdc dll Using s链接器选项有点帮助 减少了 5
  • 64 位 Windows 汇编器

    我想对 64 位 Windows 程序集进行编程 最好使用 NASM 我在 google 上查了一下 但似乎找不到 64 位 Windows 编译器 有些网站提到了ml64 但它似乎不再包含在VC 中 我尝试过 32 位程序集 但显然它在我
  • 网页优化:为什么组合文件速度更快?

    我读过 将所有 css 文件合并为一个大文件 或将所有脚本文件合并为一个脚本文件 可以减少 HTTP 请求的数量 从而加快下载速度 但我不明白这一点 我认为如果你有多个文件 最多有一个限制 我相信在现代浏览器上是 10 个 浏览器会并行下载
  • OpenMP 共享与第一私有性能比较

    我有一个 pragma omp parallel for在类方法内循环 每个线程只读访问很少的方法局部变量 很少调用私有数据和方法的参数 所有这些都在一个声明中声明shared条款 我的问题 性能方面不应该有任何区别声明这些 变量share
  • 使用非负约束进行优化

    考虑以下功能 import numpy as np import scipy optimize as opt import math Periodic indexation def pl list i return list i len l
  • SQL 执行计划是基于架构还是数据,或者两者兼而有之?

    我希望这个问题不太明显 我已经找到了很多关于解释执行计划的好信息 但有一个问题我还没有找到答案 该计划 更具体地说是相对 CPU 成本 仅基于架构 还是数据库中当前的实际数据 我尝试对我的产品数据库中需要索引的位置进行一些分析 但正在使用我
  • 将以下机器语言代码(0x2237FFF1)翻译成MIPS汇编

    到目前为止我已经翻译了这段代码 但我不明白的是如何计算 计算 16 位立即地址的数量 0x2237FFF1 转为二进制 0010 0010 0011 0111 1111 1111 1111 0001 现在我正在读取操作码 001000 并知
  • 在 qemu 中将扇区加载到 RAM

    我编写了一个简单的程序 将扇区 扇区编号 2 加载到 RAM 但什么也没打印 首先 我尝试了以下引导扇区代码 org 0x7c00 mov ax 0x1000 ES BX 1000 0000 mov es ax mov bx 0x00 Lo
  • 优化 Haskell 内循环

    仍在 Haskell 中进行 SHA1 实现 我现在已经有了一个有效的实现 这是内部循环 iterateBlock Int gt Word32 gt Word32 gt Word32 gt Word32 gt Word32 gt Word3
  • 如何恢复 x86-64 寄存器保存约定

    fibonacci cmpq 1 rdi ja recursive movl 1 eax ret recursive push rbp push r10 movq rdi r10 leaq 2 rdi rdi call fibonacci
  • 测试 xmm/ymm 寄存器是否为零的更快方法?

    It s fortunate that PTEST does not affect the carry flag but only sets the rather awkward ZF also affects both CF and ZF
  • NASM:如何正确访问SSD驱动器?

    我需要使用 NASM 16 位代码访问 SSD 驱动器 访问普通硬盘时 需要设置寄存器AX DX CX来选择柱面 磁道 扇区 扇区数 AH 选择读扇区功能 DL 选择驱动器号 CH 选择气缸 DH 选择磁盘上的一侧 CL 选择步入正轨的部门
  • 如何加速我的 Perl 程序?

    这确实是两个问题 但它们非常相似 为了简单起见 我想我应该把它们放在一起 Firstly 给定一个已建立的 Perl 项目 除了简单的代码优化之外 还有哪些不错的方法可以加速它 Secondly 用Perl从头开始编写程序时 有哪些好的方法
  • 为什么我应该使用内联代码? [复制]

    这个问题在这里已经有答案了 我是一名 C C 开发人员 这里有几个始终困扰我的问题 常规 代码和内联代码之间有很大区别吗 主要区别是什么 内联代码只是宏的一种 形式 吗 选择内联代码时必须进行什么样的权衡 Thanks 表现 正如之前的答案
  • 微软怎么能说WinAPI中一个字的大小是16位呢?

    我刚刚开始学习WinAPI 在MSDN中 对WORD数据类型提供了以下解释 WORD16 位无符号整数 范围是十进制 0 到 65535 该类型在 WinDef h 中声明如下 typedef 无符号短 WORD 很简单 而且它与我一直在使
  • 如何阅读英特尔操作码符号

    我正在阅读一些引用的材料Intel vol 2 SDM x86 手册 https www intel com content www us en developer articles technical intel sdm html关于汇编
  • 优化 tribool 数组的空间

    让我从一些背景开始 通过 tribool 我理解一个可以保存以下值之一的变量 true false or null 有问题复制整数数组与布尔指针数组 https stackoverflow com questions 4350041 cop
  • 为什么这个函数在额外读取内存时运行速度如此之快?

    我目前正在尝试了解 x86 64 上某些循环的性能属性 特别是我的 Intel R Core TM i3 8145U CPU 2 10GHz 处理器 具体来说 在循环体内添加一条额外的指令来读取内存几乎可以使性能提高一倍 而细节并不是特别重

随机推荐

  • Redis Cluster 与 Pub/Sub 中的 ZeroMQ,用于水平扩展的分布式系统

    如果我要设计一个巨大的分布式系统 其吞吐量应随系统中的订阅者数量和通道数量线性扩展 哪个会更好 1 Redis集群 仅适用于Redis 3 0 alpha 如果是集群模式 您可以在一个节点上发布并在另一个完全不同的节点上订阅 消息将传播并到
  • 无法为 Python 3.4 创建工作虚拟环境

    I 安装Python 3 4 2 https docs python org 3 using unix html building python和我的 Linux Mint 17 1 中的 Virtualenv 12 0 5 然后我尝试创建
  • 我是否可以通过第三方支付网关为我的 iPhone 应用程序提供付款服务?

    所以我有一个 RESTful Api 服务 它有免费和付费的东西 任何人都可以利用我们的 API 创建 iPhone Andriod MSPhone 应用程序 不好的类比 假设我们正在为 Steam 创建一个聊天 api 服务 并且您可以为
  • 从通用列表中删除项目

    我有以下方法 我希望从我的收藏中删除与产品 ID 匹配的项目 看起来相当简单 但我有一个例外 基本上我的收藏已经不同步了 那么从集合中删除项目的最佳方法是什么 public void RemoveOrderItem Model Order
  • 在最后修改的区域扩展Jtree?

    我正在使用 dom4j 从 dom4j 文档创建 DocumentTreeModel 我在里面显示这个 DocumentTreeModelJScrollPane 我有一个按钮 可以将新节点添加到 dom4j 文档 并重新创建 Documen
  • 元素不存在,尽管它具有 ID 属性

    在 selenium excel vba 中 我试图了解有关如何处理 CSS 选择器的更多信息 我很想知道 因为在检查带有 ID 的元素并运行代码时 我收到一条消息 指出未找到该元素 这是到目前为止的代码 Private bot As Ne
  • DC-sunburst、dc-Menuslect、dc-Non 交互图

    我是 dc js 的新手 我对 dc 的灵活性有一些疑问 首先 我一直在寻找答案 但还没有找到任何答案 1 我正在使用 dc sunburst 图表 我想知道是否可以创建 Zoomable sunburst 因为 d3 js 实际上就是这种
  • `printf()` 中格式说明符“%qd”的用途是什么?

    我看到格式说明符 qd浏览时github https github com Microsoft clang blob master test Sema format strings c代码 然后我检查了 GCC 编译器 它工作正常 incl
  • recvfrom() 中的 addrlen 字段有何用途?

    我在程序中使用 recvfrom 从我在 src addr 中指定的服务器获取 DGRAM 数据 但是 我不确定为什么需要初始化并传入addrlen 我读了手册页 但不太明白它的意思 如果src addr不为NULL 并且底层协议提供了源地
  • Powershell Bash/Zsh 命令中的多个参数

    无法在 Powershell 中运行以下 Bash Zsh 命令 KeyPath Join Path Path this Plate ChildPath install tekton key kubectl create secret do
  • NSUndoManager 会撤消后台发生的更改吗?

    我有一个编辑视图控制器 我正在使用 NSUndoManager 它是我的持久性存储 核心数据项目 的一组 我的应用程序的功能之一是与外部服务器同步 我想知道的是 如果我正在视图中编辑某些内容 同时应用程序正在与服务器同步 如果我改变主意并决
  • 如何在java中访问USB端口[关闭]

    Closed 这个问题正在寻求书籍 工具 软件库等的推荐 不满足堆栈溢出指南 help closed questions 目前不接受答案 我正在尝试编写一个java应用程序来访问USB端口以读取和写入通过USB连接的设备 我面临的问题是我不
  • jQuery UI 滑动轻松同级推送

    我正在使用 jQuery UIslide切换 div 的切换效果 link click function targetDiv toggle slide direction up 1000 幻灯片是唯一具有我想要的动画的效果 本质上是 div
  • 我应该使用 和 吗?如果是,为什么以及如何使用?

    我一直在尝试正确使用 colgroup 和 col 标签 但我不明白 我阅读了规范和所有内容 但我不明白其目的或如何实现它 A colgroup用于table元素来帮助理解具有不规则标题的表中复杂的信息层次结构 WAI 有一个关于如何处理此
  • 在运行时更改蓝图或重新加载 Flask 应用程序

    我正在编写一个支持插件架构的 Flask 应用程序 每个插件都位于一个单独的文件夹中 并且是一个模块 该模块至少具有一个类 该类是一个子类Plugin班级 出于安全原因 我不想在 Flask 应用程序最初运行时加载所有插件 相反 用户可以从
  • Three.js 椭圆

    如何在 Three js 中创建一个椭圆 我看过这个 在 THREE js 中绘制椭圆 https stackoverflow com questions 11419896 drawing an ellipse in three js 但如
  • ndb.StructuredProperty 不调用 ndb.PolyModel 子类方法

    在将 ndb Polymodel 超类存储为 ndb StructuredProperty 时 我无法访问子类方法 相反 调用超类方法并引发 NotImplementedError 这是我想要完成的任务的删节版本 class Recipie
  • 是否可以使用Kafka传输文件?

    我每天都会生成数千个文件 我想使用 Kafka 进行流式传输 当我尝试读取该文件时 每一行都被视为一条单独的消息 我想知道如何将每个文件的内容作为 Kafka 主题中的单个消息 以及消费者如何将 Kafka 主题中的每条消息写入单独的文件中
  • QML 项目的 QtCreator 中未启用“运行”按钮

    我在Windows XP上使用基于QT 4 7 4 32位 的QTCreator 2 2 1 我从 new gt QML 项目菜单创建了一个 QML 项目 但 RUN 按钮未启用 如何运行 QML 项目 您是否创建了新的 QML 文件而不是
  • 为什么在强度降低乘法和循环进位加法之后,这段代码的执行速度会变慢?

    我正在读书阿格纳 雾 https en wikipedia org wiki Agner Fog s 优化手册 https en wikipedia org wiki Agner Fog Optimization 我遇到了这个例子 doub