【问题标题】:Performance penalty with executing x86 instructions stored in the data segment?执行存储在数据段中的 x86 指令的性能损失?
【发布时间】:2010-12-08 22:00:24
【问题描述】:

我有一个简单的程序,它首先将一些本机 x86 指令写入声明的缓冲区,然后设置一个指向该缓冲区的函数指针并进行调用。但是,当在堆栈上分配缓冲区时(而不是在堆上,甚至在全局数据区域中),我注意到严重的性能损失。我验证了数据缓冲区中指令序列的开始位于 16 字节边界上(我假设这是 cpu 需要(或希望)它是什么)。我不知道为什么在我的过程中执行指令会有所不同,但在下面的程序中,“GOOD”在我的双核工作站上执行 4 秒,“BAD”需要 6 分钟左右.这里是否存在某种对齐/i-cache/预测问题?我对 VTune 的评估许可刚刚结束,所以我什至无法对此进行分析:(。谢谢。


#include <stdio.h>
#include <string.h>
#include <stdlib.h>

typedef int (*funcPtrType)(int, int);

int foo(int a, int b) { return a + b; }

void main()
{
  // Instructions in buf are identical to what the compiler generated for "foo".
  char buf[201] = {0x55,
                   0x8b, 0xec,
                   0x8b, 0x45, 0x08,
                   0x03, 0x45, 0x0c,
                   0x5D,
                   0xc3
                  };

  int i;

  funcPtrType ptr;

#ifdef GOOD
  char* heapBuf = (char*)malloc(200);
  printf("Addr of heap buf: %x\n", &heapBuf[0]);
  memcpy(heapBuf, buf, 200);
  ptr = (funcPtrType)(&heapBuf[0]);
#else // BAD
  printf("Addr of local buf: %x\n", &buf[0]);
  ptr = (funcPtrType)(&buf[0]);
#endif

  for (i=0; i < 1000000000; i++)
    ptr(1,2);
}

运行结果如下:

$ cl -DGOOD ne3.cpp
Microsoft (R) 32 位 C/C++ 优化编译器版本 11.00.7022 用于 80x86
版权所有 (C) Microsoft Corp 1984-1997。版权所有。

ne3.cpp
Microsoft (R) 32 位增量链接器版本 5.10.7303
版权所有 (C) Microsoft Corp 1992-1997。版权所有。

/out:ne3.exe
ne3.obj
$ 时间 ./ne3
堆缓冲区地址:410eb0

真正的 0m 4.33s
用户 0m 4.31s
系统 0m 0.01s
$
$
$ cl ne3.cpp
Microsoft (R) 32 位 C/C++ 优化编译器版本 11.00.7022 用于 80x86
版权所有 (C) Microsoft Corp 1984-1997。版权所有。

ne3.cpp
Microsoft (R) 32 位增量链接器版本 5.10.7303
版权所有 (C) Microsoft Corp 1992-1997。版权所有。

/out:ne3.exe
ne3.obj
$ 时间 ./ne3
本地 buf 地址:12feb0

真正的 6m41.19s
用户 6m40.46s
系统 0m 0.03s
$

谢谢。

  • 沙桑克

【问题讨论】:

  • 顺便说一句,还要检查生成的代码。虽然看起来不太可能,但谁真正知道编译器是否只在一个路径 中进行了优化。 . . 和通过几个循环的单步操作。 “这是唯一确定的方法。”

标签: performance caching x86 alignment execution


【解决方案1】:

堆栈保护安全?

大胆猜测,您可能会遇到基于 MMU 的堆栈保护方案。许多安全漏洞是基于故意的缓冲区溢出,将可执行代码注入堆栈。解决这些问题的一种方法是使用不可执行的堆栈。这将导致进入操作系统的陷阱,我认为操作系统或某些病毒软件可能会做一些事情。

负 i-cache 一致性交互?

另一种可能性是同时使用代码和数据访问附近地址会破坏 CPU 缓存策略。我相信 x86 实现了一个本质上自动的代码/数据一致性模型,这很可能导致任何内存写入时附近的大量缓存指令失效。您无法通过将程序更改为不使用堆栈来真正解决此问题(显然您可以移动动态代码),因为堆栈始终由机器代码编写,例如,每当推送参数或返回地址时过程调用。

如今,相对于 DRAM 甚至外层缓存环,CPU 的速度非常快,因此任何破坏内层缓存环的事情都会非常严重,而且它的实现可能涉及 CPU 实现中的某种微陷阱,然后是硬件中的“循环”以使事物无效。这不是 Intel 或 AMD 会担心速度的事情,因为对于大多数程序来说,它永远不会发生,而且当它发生时,通常只会在加载程序后发生一次。

【讨论】:

  • 真是好主意,伙计们。所以,为了防止这种情况发生(注意,从你的意思来看,我认为即使指令存储在堆中也会发生这种情况),指令应该位于数据缓冲区的中间,该缓冲区在两个通过“cache-line-size”字节指示,以便写入相邻数据不会弄脏包含我的指令的缓存行?再次感谢您的快速回复。 - 沙桑克
  • 我不能肯定。在 8086 上运行的软件不需要担心缓存,因此出于向后兼容的原因,英特尔决定在硬件中进行大量缓存管理,这很复杂。我敢肯定有一些这方面的专家,但对于你我这样的人来说:测试代码,看看它是如何执行的!
  • 好吧,在你的问题下面查看我的评论......你肯定想要单步处理堆和堆栈版本,以确保你认为正在发生的事情真的发生了。仅仅将缓存块写入远离代码可能还不够好,一致性粒度可以是页面大小或设计人员想要的任何内容。一个有趣的想法是在堆栈上分配一些相当大的东西......几个页面。看看是 4k 字节还是 40k 字节有帮助...
【解决方案2】:

我的猜测是,由于您在堆栈上也有变量 i,所以当您在 for 循环中更改 i 时,您会丢弃代码所在的同一缓存行。把代码在缓冲区的中间某处(可能会扩大缓冲区),以使其与其他堆栈变量分开。

另请注意,在堆栈上执行指令通常是安全漏洞(例如缓冲区溢出)被利用的标志。

因此,操作系统通常被配置为禁止这种行为。病毒扫描程序也可能对其采取措施。也许您的程序每次尝试访问该堆栈页面时都在通过安全检查(尽管在这种情况下我希望 sys 时间字段更大)。

如果您想“正式”使内存页可执行,您可能应该查看VirtualProtect()

【讨论】:

  • 谢谢阿特柳斯。我在 DigitalRoss 的回答下添加了评论。 - 沙桑克
  • 如果你想检查这个你可以做的是在bufi之间放置一大块数据(在buf和@987654331的声明之间使用char junk[10000]之类的东西@) 这将确保它们不在同一个缓存行中。这样你就不会遇到自我修改代码,这对性能来说真的非常非常糟糕。
  • 虽然要注意编译器不会优化它:P
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-01-11
  • 1970-01-01
  • 2019-02-09
  • 2011-08-24
  • 2019-10-02
相关资源
最近更新 更多