【问题标题】:How many instructions to access pointer in C?C语言中访问指针的指令有多少?
【发布时间】:2011-02-14 09:45:45
【问题描述】:

我试图弄清楚访问 C 中的指针需要多少时钟周期或总指令。我想我不知道如何计算,例如 p->x = d->a + f-> b

我会假设每个指针有两个负载,只是猜测会有一个负载用于指针,一个负载用于值。所以在这个操作中,指针解析将是一个比实际加法更大的因素,就试图加快这段代码的速度,对吧?

这可能取决于实现的编译器和架构,但我是否走在正确的轨道上?

我看过一些代码,其中使用的每个值,比如 3 个加法,都来自一个

 f2->sum = p1->p2->p3->x + p1->p2->p3->a + p1->p2->p3->m

结构类型,我试图定义这是多么糟糕

【问题讨论】:

  • 取决于地址模式恕我直言 - 近跳转/长跳转,地址计算...
  • 记住编译器应该在获取一次后将其中的大部分移到堆栈中。如果不是,而且你不需要担心多线程,你可以自己缓存指针追逐。
  • @Robert:如果多线程会影响示例中的指针取消引用,那么代码需要显式序列化 - 优化编译器将始终能够将 p3 缓存到寄存器中并将其用于所有 3 个成员访问(假设没有使用 volatile 成员)。
  • @Michael Burr - 是的;我正是这个意思。 +1 解释。

标签: c pointers instructions


【解决方案1】:

这取决于手头的架构。

有些架构可以为指令引用/取消引用内存,而无需先将其加载到寄存器中,而其他架构则不能。一些体系结构没有计算偏移量的指令概念以供您取消引用,并且会让您加载内存地址,将偏移量添加到它,然后允许您取消引用内存位置。我敢肯定芯片到芯片会有更多的差异。

一旦您通过了这些,每条指令所花费的时间也会有所不同,具体取决于架构。不过老实说,这是非常非常少的开销。

对于取消引用一系列项目的直接问题,速度缓慢的原因是,您在取消引用链中走得越远,引用的局部性可能就越差。这意味着更多的缓存未命中,这意味着对主内存(或磁盘!)的更多命中以获取数据。与 CPU 相比,主内存非常慢。

【讨论】:

  • 我不认为它是最小的。在优化上述代码时,我已经看到 3 - 8 倍的加速摆脱了指针并使用正常的数组访问。如果指针实际上是结构,问题就更严重了。
  • @derek 首先,如果代码不断执行,这只是一个潜在的不良开销,在这种情况下,除非你正在破坏缓存,否则应该在 DTLB 中缓存连续的内存查找(在x86 的情况)。尽可能使用寄存器仍然很好,这是编译器所做的。我的答案中的示例表明,即使将局部变量相互分配,也可以进行指针访问。
  • @Derek:这将来自于改进缓存局部性。
  • @derek 我不买。在一个公平的竞争环境中,它实际上是另一条或两条指令,实际上通常非常相似。通常,您将加载数组的起始点(或指针的值),然后从那里简单地使用索引寄存器之类的东西计算偏移量。通过指针的指针和数组都会发生这种情况。如果您谈论的是链表或树,那就是局部性问题。
  • @San Jacinto 我写了一个非常简单的程序,有 2 个三个结构。编译时未对 MIPS 编译器 -S 选项进行优化。 p.something->nextthing->value 的汇编程序在每次访问时计算为三个加载。编译器没有存储 nextthing,只是从那里访​​问值。
【解决方案2】:

取决于你在做什么,一个简单的指针取消引用y = *z; where

int x = 1;
int* z = &x;
int y;

可能会在 x86 上组装成这样的东西:

mov eax, [z]
mov eax, [eax]
mov [y], eax

y = x 仍然会进行内存取消引用:

mov eax, [x]
mov [y], eax

将指令移动到内存大约需要 2-4 个周期 IIRC。

虽然,如果您从完全随机的位置加载内存,则会导致大量页面错误,从而导致 数百 个时钟周期被浪费。

【讨论】:

    【解决方案3】:

    VisualStudio 等一些 IDE 允许您查看随源代码生成的程序集。

    How to view the assembly behind the code using Visual C++?

    然后您可以查看您的确切架构和实现它的样子。

    如果您使用 GDB(linux、mac),请使用 disassemble

    (gdb) disas 0x32c4 0x32e4
    Dump of assembler code from 0x32c4 to 0x32e4:
    0x32c4 <main+204>:      addil 0,dp
    0x32c8 <main+208>:      ldw 0x22c(sr0,r1),r26
    0x32cc <main+212>:      ldil 0x3000,r31
    0x32d0 <main+216>:      ble 0x3f8(sr4,r31)
    0x32d4 <main+220>:      ldo 0(r31),rp
    0x32d8 <main+224>:      addil -0x800,dp
    0x32dc <main+228>:      ldo 0x588(r1),r26
    0x32e0 <main+232>:      ldil 0x3000,r31
    End of assembler dump.
    

    【讨论】:

    • 我使用 -S 选项进行编译,发现与其他人所说的非常相似。
    【解决方案4】:

    在可能的情况下,编译器将通过将重复使用的基地址保存在寄存器中(例如,您的示例中的 p1-&gt;p2-&gt;p3)为您消除开销。

    但是,有时编译器无法确定哪些指针可能别名函数中使用的其他指针 - 这意味着它必须回退到一个非常保守的位置,并经常从指针重新加载值.

    这就是 C99 的 restrict 关键字可以提供帮助的地方。它可以让您在某些指针永远不会被函数范围内的其他指针别名时通知编译器,从而可以改进优化。


    以这个函数为例:

    struct xyz {
        int val1;
        int val2;
        int val3;
    };
    
    struct abc {
        struct xyz *p2;
    };
    
    int foo(struct abc *p1)
    {
        int sum;
    
        sum = p1->p2->val1 + p1->p2->val2 + p1->p2->val3;
    
        return sum;
    }
    

    在优化级别-O1 的gcc 4.3.2 下,它编译成这个x86 代码:

    foo:
        pushl   %ebp
        movl    %esp, %ebp
        movl    8(%ebp), %eax
        movl    (%eax), %edx
        movl    4(%edx), %eax
        addl    (%edx), %eax
        addl    8(%edx), %eax
        popl    %ebp
        ret
    

    如您所见,它只引用 p1 一次 - 它将 p1-&gt;p2 的值保存在 %edx 寄存器中,并使用它三次从该结构中获取三个值。

    【讨论】:

    • 我实际上写了一个测试程序,用 -S 选项编译,我发现即使对于一个简单的情况,例如 p1.p2->p3->value 或类似的东西,它从重新加载p1 每次。非常保守,没有优化
    • @Derek:你使用了什么优化级别?使用-O1 或更高版本,它应该可以很好地优化简单的情况(请参阅我添加到答案中的示例)。
    • 是的,它会,但是程序越复杂,它就会失去一些能力。这就是我的观点
    猜你喜欢
    • 2021-07-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-09-23
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多