编辑
“这取决于”是答案。首先,有些缓存将 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
根本没有数据访问。