【问题标题】:To which cache a function pointer belongs to?函数指针属于哪个缓存?
【发布时间】:2015-06-11 16:03:34
【问题描述】:

在 C 中,如果我有一个函数指针

int (*f_ptr) (int)

它将在指令缓存中还是在数据缓存中?在这两个缓存中找到f_ptr 我不会感到惊讶。有一种方法可以在 linux 下调试这个,可能使用perf,有点像数据缓存、指令缓存和翻译后备缓冲区的鸟瞰图?

【问题讨论】:

  • 它和其他变量一样是一个变量——它在数据缓存中。例如,如果你有一个函数指针数组,你认为把它放在指令缓存中有意义吗?
  • @FilipeGonçalves 一个数组可能没有,但单个函数指针是的,例如考虑到f_ptr 部分签名的foo( int (*f_ptr) (int) ) 至少对我来说,得到@987654326 是有意义的@inst.缓存。
  • 函数指针仍然是一个指针,即基本上只是一些地址。 CPU 不会执行地址——实际的指令序列将沿着load <register> from [pointer]; call <register> 的行。诚然,x86 可以像call [address of pointer] 那样做这件事有点混乱,但即便如此,指针本身也不是指令流的一部分。
  • @Notlikethat 我的推理是关于 CPU 可以对函数式语言的解释器应用什么样的优化,这些语言倾向于优先考虑函数而不是值,我正在考虑模拟类似的东西在 C 中,我期望 CPU 比实际的更智能。
  • 我认为这取决于它的使用方式,以及优化如何影响生成的机器代码。它有可能变成“立即加载”指令,在这种情况下,指针值成为指令流的一部分,你会在指令缓存中找到它。但它也可以存储在数据段中的内存位置,在这种情况下,它将在数据缓存中......

标签: c caching x86 arm intel


【解决方案1】:

函数执行时,函数的代码会进入指令缓存。

但我假设您在谈论函数指针变量本身。由于它是一个变量,它最终会出现在数据缓存中。它只是一个包含地址的变量。简而言之就是一个指针。

【讨论】:

    【解决方案2】:

    I$(指令缓存)仅由 CPU 前端的指令获取逻辑使用。 mov 的内存操作数和任何其他指令都放在 L1 D$ 中。

    使用memcpy 复制代码块会将其(部分)留在 L1 D$(和 L2 / L3 缓存)中。跳转到它甚至不会查看 D$:指令提取管道将开始将其提取到 L1 I$。幸运的是,L2 和 L3 是统一的(未拆分为代码/数据),因此指令获取会在 L2 中命中。 (除非代码块太大以至于在memcpy 完成时开始被驱逐。)

    拆分 (I$ / D$) L1 和统一其他级别是 CPU 设计的一个非常普遍的选择,而不仅仅是 Intel / AMD x86 CPU。

    为了真正回答这个问题,函数指针存储了一个地址。这将永远不会存储在 I$ 中。如果您调用指向的函数(使用call 指令将CPU 的指令指针设置为该值),它将指向的内存将被提取到I$ 中。

    【讨论】:

      【解决方案3】:

      编辑

      “这取决于”是答案。首先,有些缓存将 I 和 D 组合在一起,有些将它们分开,有些是可配置的。

      底线是启用缓存的数据访问是对数据缓存的数据访问,启用缓存的指令获取连接到指令缓存。

      除非优化到寄存器中,否则对函数指针的操作是对数据缓存的数据访问。执行该函数需要该地址,因此要么将其优化为寄存器并且不需要访问,要么需要数据访问来获取地址,然后调用/分支到该地址,从而导致该地址处的指令成为候选用于指令缓存。

      函数指针用于可加载模块或抽象层的情况并不少见,其中一次您将函数的地址更改为指向加载的模块等,然后多次调用这些函数。根据您的系统和应用程序,调用该函数之间的时间可能会导致其他数据访问将该函数指针地址驱逐到 L2 或 L3 ...并最终将主/慢 ram,在这种情况下,它不再位于任何缓存中.当最终调用发生时,数据访问发生,地址被读取并落在它通过的缓存层中(不管你有多少)。

      一些简单的例子:

      int (*f_ptr) (int);
      
      int fun ( int x )
      {
          return(f_ptr(5)+x);
      }
      

      一个编译器产生:

      00000000 <fun>:
         0:   e59f3018    ldr r3, [pc, #24]   ; 20 <fun+0x20>
         4:   e92d4010    push    {r4, lr}
         8:   e5933000    ldr r3, [r3]
         c:   e1a04000    mov r4, r0
        10:   e3a00005    mov r0, #5
        14:   e12fff33    blx r3
        18:   e0800004    add r0, r0, r4
        1c:   e8bd8010    pop {r4, pc}
        20:   00000000    andeq   r0, r0, r0
      

      正如预期的那样,读取指针的地址是一次数据访问

       0: e59f3018    ldr r3, [pc, #24]   ; 20 <fun+0x20>
      

      不仅是预期的,而且是需要调用的

      14: e12fff33 blx r3

      将是该地址的指令获取。在上面的示例是 L1 的手臂上,可以/结合了 I 和 D,所以如果对该地址的写入足够近并且没有被驱逐,那么它会将其从缓存中拉出。否则 l2 或 l3 ...或主/慢 ram

      现在做这样的事情:

      int (*f_ptr) (int);
      
      int ext_fun ( int );
      
      int more_fun ( int y )
      {
          return(ext_fun(y));
      }
      
      int fun ( int x )
      {
          f_ptr = more_fun;
          return(f_ptr(5)+x);
      }
      

      给了

      00000000 <more_fun>:
         0:   eafffffe    b   0 <ext_fun>
      
      00000004 <fun>:
         4:   e59f301c    ldr r3, [pc, #28]   ; 28 <fun+0x24>
         8:   e59f201c    ldr r2, [pc, #28]   ; 2c <fun+0x28>
         c:   e92d4010    push    {r4, lr}
        10:   e1a04000    mov r4, r0
        14:   e3a00005    mov r0, #5
        18:   e5832000    str r2, [r3]
        1c:   ebfffffe    bl  0 <ext_fun>
        20:   e0840000    add r0, r4, r0
        24:   e8bd8010    pop {r4, pc}
          ...
      

      所以 f_ptr 的地址被存储了,但实际上并没有被读取或使用。

      这样做

      int (*f_ptr) (int);
      
      int more_fun ( int y )
      {
          return(y+7);
      }
      
      int fun ( int x )
      {
          f_ptr = more_fun;
          return(f_ptr(5)+x);
      }
      

      00000000 <more_fun>:
         0:   e2800007    add r0, r0, #7
         4:   e12fff1e    bx  lr
      
      00000008 <fun>:
         8:   e59f300c    ldr r3, [pc, #12]   ; 1c <fun+0x14>
         c:   e59f200c    ldr r2, [pc, #12]   ; 20 <fun+0x18>
        10:   e280000c    add r0, r0, #12
        14:   e5832000    str r2, [r3]
        18:   e12fff1e    bx  lr
          ...
      

      存储发生了,但根本没有函数调用

      有趣:

      static int (*f_ptr) (int);
      
      static int more_fun ( int y )
      {
          return(y+7);
      }
      
      int fun ( int x )
      {
          f_ptr = more_fun;
          return(f_ptr(5)+x);
      }
      

      编译器错过了一个机会

      00000000 <more_fun>:
         0:   e2800007    add r0, r0, #7
         4:   e12fff1e    bx  lr
      
      00000008 <fun>:
         8:   e59f300c    ldr r3, [pc, #12]   ; 1c <fun+0x14>
         c:   e59f200c    ldr r2, [pc, #12]   ; 20 <fun+0x18>
        10:   e280000c    add r0, r0, #12
        14:   e5832000    str r2, [r3]
        18:   e12fff1e    bx  lr
          ...
      

      本可以逍遥法外

        10:   e280000c    add r0, r0, #12
        18:   e12fff1e    bx  lr
      

      因为重置是死代码。

      所以切换编译器并抓住机会

      fun:                                    @ @fun
              .fnstart
      .Leh_func_begin0:
      @ BB#0:                                 @ %entry
              add     r0, r0, #12
              bx      lr
      

      对于 f_ptr 的地址,既没有数据写入也没有读取数据。所以你不能普遍地说 f_ptr 在缓存中。

      张贴者标记了多个指令集,也许在其中我们可能同意会有一条数据指令来获取函数指针地址,然后对该地址的调用/分支将在该地址获取指令。但是有些指令集具有双重间接数据指令,可能还有那些具有跳转表指令的指令集,是的,地址的读取是数据操作,但处理器会这样工作吗?特别是如果它可能是哈佛架构并且跳转表指令是相对于指令位置的(保证在.text中的指令旁边,重新读取您可能已经获取的相同空间效率低下)。

      简短的回答,该函数的指令是指令提取,将通过 i 缓存。对函数指针的操作(包括读取它以执行调用)很可能是数据访问,并且如果缓存在 d 缓存中。除非有人认为某个指令集中的指令 f_ptr 地址将在数据缓存中,否则在 i 缓存中必须是针对某些特定处理器的特殊指令,并且编译器必须使用该指令(这会导致地址操作和地址使用之间的竞争条件,编译器将不得不导致刷新,它可能宁愿实现数据读取然后调用而不是使用特殊指令)。

      编辑 2

      地址被优化成寄存器的简单例子

      static int (*f_ptr) (int);
      
      int fun ( int x )
      {
          f_ptr = (void *)0x1000;
          return(f_ptr(5)+x);
      }
      

      给予

      fun:                                    @ @fun
          .fnstart
      .Leh_func_begin0:
      @ BB#0:                                 @ %entry
          .save   {r4, r10, r11, lr}
          push    {r4, r10, r11, lr}
          .setfp  r11, sp, #8
          add r11, sp, #8
          mov r4, r0
          mov r1, #4096
          mov r0, #5
          mov lr, pc
          bx  r1
          add r0, r0, r4
          pop {r4, r10, r11, lr}
          bx  lr
      

      根本没有数据访问。

      【讨论】:

      • 这包含几个不正确的逻辑。原始指针始终在数据缓存中。获取指针指向的指令将指令放入指令缓存中。
      • 指针并不总是在数据缓存中,它可以被驱逐到内存中,并且可能对于许多指向函数的用例来说都是如此。取决于函数被调用的频率以及地址在缓存中的频率。底线是处理器用来读取某些内容的访问方法确定它在哪个缓存中,如果传输被标记为数据然后数据如果指令获取然后指令获取它并不比这更复杂。
      • 同样重要的是传输是否被标记为可缓存,以及在情况下是什么风格,这取决于您是否使用 mmu 以及您为该地址空间设置的缓存设置.
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2016-01-17
      • 1970-01-01
      • 1970-01-01
      • 2010-10-10
      • 1970-01-01
      • 2021-06-03
      • 2014-08-13
      相关资源
      最近更新 更多