在C中,如果我有一个函数指针
int (*f_ptr) (int)
它将在指令缓存中还是在数据缓存中?在任何一个缓存中找到f_ptr
都不会感到惊讶。有一种方法可以在linux下用perf
调试这个,有点像鸟瞰数据缓存,指令缓存和转换后备缓冲区吗?
答案 0 :(得分:0)
执行该功能时,该函数的代码将进入指令缓存。
但我假设您正在讨论函数指针变量本身。由于它是一个变量,它最终会在数据缓存中结束。它只是一个包含地址的变量。简而言之。
答案 1 :(得分:0)
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 $中。
答案 2 :(得分:-1)
修改
"它取决于"是答案。首先,一些缓存有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完全在缓存中。
海报标记了多个指令集,也许在那些指令集中我们可能会同意将有一个数据指令来获取函数指针地址,然后对该地址的调用/分支将获取该地址的指令。但是有些指令集有双重间接数据指令,也许有跳转表指令,是的,地址的读取是数据操作是真的,但这个处理器的工作方式是什么? esp如果它可能是一个哈佛架构并且跳转表指令是相对于指令位置的(保证在.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
根本没有数据访问。