我有一个非常特殊的问题:在C / ++中是否可以指定函数的位置(两者都是因为我确信这两种语言的问题都相同)?为什么?我有很多函数指针,我想消除它们。
(当前)如下所示(重复一百万次lika,存储在用户的RAM中):
riwayat_pinjam
因为我知道在大多数汇编语言中,函数只是“ goto”指令,所以我有以下想法。是否可以优化上述结构,使其看起来像这样?
struct {
int i;
void(* funptr)();
} test;
最后,事情应该在内存中看起来像这样:一个包含任何值的int,后跟test2引用的函数的汇编代码。我应该能够像这样调用这些函数:struct {
int i;
// embed the assembler of the function here
// so that all the functions
// instructions are located here
// like this: mov rax, rbx
// jmp _start ; just demo code
} test2;
您可能会认为我以这种方式优化应用程序非常疯狂,而且我无法透露其功能的更多细节,但是如果有人对如何解决此问题有任何建议,我将不胜感激。 我不认为有一种标准的方法,因此通过内联汇编器/其他疯狂的方式进行此操作的任何骇人听闻的方法也将受到赞赏!
答案 0 :(得分:2)
您唯一需要做的就是让编译器知道您要在结构中使用的函数指针的(常量)值。然后,编译器将(大概/希望地)内联该函数调用,只要它通过该函数指针看到它被调用的地方:
template<void(*FPtr)()>
struct function_struct {
int i;
static constexpr auto funptr = FPtr;
};
void testFunc()
{
volatile int x = 0;
}
using test = function_struct<testFunc>;
int main()
{
test::funptr();
}
Demo-优化后没有call
或jmp
。
目前尚不清楚int i
的意义。请注意,该代码并不是在技术上“直接在i
之后”,但是更不清楚的是您希望结构的 instances 是什么样(代码是否在其中)?还是在某种程度上是“静态的”?我觉得您的编译器实际上产生了一些误解……)。但是考虑一下编译器内联可以为您提供帮助的方式,您可能会找到所需的解决方案。如果您担心内联后的可执行文件大小,请告诉编译器,它将在速度和大小之间妥协。
答案 1 :(得分:2)
由于很多原因,这听起来像是一个可怕的主意,可能无法节省内存,并且会因使用数据稀释L1I缓存和使用代码稀释L1D缓存而损害性能。您曾经修改或复制对象:自修改代码停顿。
但是,是的,在C99 / C11中,可以在结构的末尾使用灵活的数组成员,并将其转换为函数指针。
struct int_with_code {
int i;
char code[]; // C99 flexible array member. GNU extension in C++
// Store machine code here
// you can't get the compiler to do this for you. Good Luck!
};
void foo(struct int_with_code *p) {
// explicit C-style cast compiles as both C and C++
void (*funcp)(void) = ( void (*)(void) ) p->code;
funcp();
}
编译器输出from clang7.0, on the Godbolt compiler explorer与C或C ++编译时相同。这是针对x86-64 System V ABI的,其中第一个函数arg在RDI中传递。
# this is the code that *uses* such an object, not the code that goes in its code[]
# This proves that it compiles,
# without showing any way to get compiler-generated code into code[]
foo: # @foo
add rdi, 4 # move the pointer 4 bytes forward, to point at code[]
jmp rdi # TAILCALL
(如果您省略了C中的(void)
arg类型声明,则编译器将在x86-64 SysV调用约定中首先将AL零,以防它实际上是一个可变参数,因为它没有传递FP args在寄存器中。)
您必须在可执行的内存中分配对象(通常不会这样做,除非它们const
具有静态存储空间),例如用gcc -zexecstack
进行编译。或在POSIX或Windows上使用自定义的mmap / mprotect或VirtualAlloc / VirtualProtect。
或者,如果所有对象都是静态分配的,则可以通过在每个.text
部分之前添加一个int
成员,来编译编译器输出以将.section
部分中的函数转换为对象。也许有一些char code[60]
和链接器技巧,甚至还有链接器脚本,您甚至可以以某种方式使它自动化。
但是,除非它们的长度都相同(例如,使用ret
这样的填充),否则就不会形成可以索引的数组,因此您需要某种方式来引用所有这些可变长度对象
如果在调用对象的函数之前对其进行修改,则可能会带来巨大的性能下降:在x86上,您将获得用于执行代码的自修改代码管道核对,在刚编写的内存位置执行 near
或者,如果您在调用对象的功能之前复制了对象:x86管道刷新,或在其他ISA上,则需要手动刷新缓存以使I缓存与D缓存同步(以便可以执行新写入的字节) )。 但是您不能复制此类对象,因为它们的大小没有存储在任何地方。您无法在机器代码中搜索0xc3
指令,因为ret
字节可能出现在x86指令的开头以外的地方。或者在任何ISA上,该函数可能具有多个__builtin___clear_cache
指令(尾部重复优化)。或者以jmp代替ret(tailcall)结束。
存储大小将开始破坏保存大小的目的,在每个对象中至少占用一个额外的字节。
在运行时将代码写入对象,然后转换为函数指针是ISO C和C ++中未定义的行为。在GNU C / C ++上,请确保在其上调用malloc
以同步缓存或进行其他必要的操作。是的,即使在x86上也需要禁用死区消除优化:see this test case。在x86上,它只是编译时的东西,没有多余的asm。实际上并不会清除任何缓存。
如果在运行时启动时确实进行复制,则可能在复制时分配很大的内存块并切出可变长度的块。如果您分别lea eax, [rdi + rdi*2]
,那么您将浪费内存管理开销。
通常,您的实际功能数量有限,许多对象具有相同功能指针的副本。 (您有种手动滚动的C ++虚拟函数,但是只有一个函数,您直接拥有一个函数指针,而不是指向该类类型的指针表的vtable指针。少一层的间接访问,显然您可以不要将对象自己的地址传递给函数。)
这种间接级别的诸多好处之一是,一个指针通常比函数的整个代码小得多。为了避免这种情况,您的功能必须是 tiny 。
示例:具有10个不同的函数(每个函数32个字节)和1000个带有函数指针的对象,您总共有320个字节的代码(将在I高速缓存中保持高温)和8000个字节的函数指针。 (在您的对象中,每个对象在填充时浪费了4个字节来对齐指针,从而使总大小为16个而不是每个对象12个字节。)总之,整个结构+代码的总大小为 16320字节 。如果您分别分配每个对象,那么每个对象都会记账。
通过将机器代码内联到每个对象中,而无需填充,即1000 *(4 + 32)= 36000字节,超过总大小的两倍。
x86-64可能是最理想的情况,其中指针是8个字节,x86-64机器代码使用(众所周知的)可变长度指令编码,从而可以实现高代码密度在某些情况下,尤其是在优化代码大小时。 (例如代码搜索。https://codegolf.stackexchange.com/questions/132981/tips-for-golfing-in-x86-x64-machine-code)。但是,除非您的函数像ret
(3个字节=操作码+ ModRM + SIB)/ return x*3;
(1个字节)之类的琐碎小东西,否则它们仍将占用8个字节以上。 (对于在x86-64 System V ABI中采用32位整数x
arg的函数,该值为call rel32
。)
如果它们是较大功能的包装,则正常的opcode + modrm + rel32
指令为5个字节。对于RIP相对寻址模式,静态数据的加载量至少为6个字节(mov eax, [32 bit absolute address]
,或者专门加载EAX可以使用绝对地址的特殊no-modrm编码。但是在x86-64中,这是64-位绝对值,除非您也使用地址大小前缀,否则可能会导致Intel的解码器中的LCP停顿。foo()
= addr32(0x67)+操作码+ abs32 = 6字节,因此这会变得更糟,无济于事)
您的函数指针类型没有任何args(假设这是C ++,其中foo(void)
在声明中表示(...)
,而不是像旧C那样,空arg列表与{{ 1}})。因此,我们可以假设您没有传递args,因此为了做有用的事情,这些函数可能正在访问一些静态数据或进行另一个调用。
使用类似Linux x32的ILP32 ABI,其中CPU以64位模式运行,但是您的代码使用32位指针。通常,这将使每个对象只有8个字节而不是16个字节。通常,避免指针膨胀是x32或ILP32 ABI的经典用例。
或者(糟糕)将您的代码编译为32位。但是,您将拥有过时的32位调用约定,这些约定将args传递给堆栈而不是寄存器,并且传递不到寄存器的一半,并且位置无关代码的开销也更高。 (没有EIP / RIP相对寻址。)
将unsigned int
表索引存储到函数指针表中。如果您有100个函数但有10k个对象,则该表只有100个指针。在asm中,如果所有函数都填充为相同的长度,则可以直接为代码数组(计算的goto样式)建立索引,但是在C ++中,您不能这样做。最好使用函数指针表进行间接访问。
例如
void (*const fptrs[])(void) = {
func1, func2, func3, ...
};
struct int_with_func {
int i;
unsigned f;
};
void bar(struct int_with_func *p) {
fptrs[p->f] ();
}
clang / gcc -O3输出:
bar(int_with_func*):
mov eax, dword ptr [rdi + 4] # load p->f
jmp qword ptr [8*rax + fptrs] # TAILCALL # index the global table with it for a memory-indirect jmp
如果您要编译共享库,PIE可执行文件或不针对Linux,则编译器不能使用32位绝对地址来通过一条指令索引静态数组。因此,那里有一个相对于RIP的LEA,类似jmp [rcx+rax*8]
。
与在每个对象中存储函数指针相比,这是一个间接的附加级别,但是它使您可以像使用32位指针一样,将每个对象的字节数从16个缩减到8个字节。如果使用unsigned short
或uint8_t
并用GNU C中的__attribute__((packed))
压缩结构,则为5或6字节。
答案 2 :(得分:1)
不,不是。
指定函数位置的方法是使用已经在做的函数指针。
您可以创建具有各自不同成员函数的不同类型,但随后又回到了原始问题。
我过去曾尝试过自动生成(作为使用Python的预构建步骤)一个带有长switch
语句的函数,该语句完成将int i
映射到普通函数的工作呼叫。这摆脱了函数指针,但以分支为代价。我不记得它最终对我来说是否值得,即使我做到了,也无法告诉我们在你的案例中是否值得。
因为我知道在大多数汇编语言中,函数只是“ goto”指令
好吧,这也许比这更复杂
您可能会认为我疯狂地优化应用程序
也许。尝试消除间接本身并不是一件坏事,因此,我认为尝试改善这一点是不对的。我只是认为您不一定可以。
但是如果有人有一些指针
哈哈
答案 3 :(得分:0)
我不了解这种“优化”的目的是关于节省内存吗?
我可能会误解这个问题,但是如果您只是用常规函数替换函数指针,那么您的结构将仅包含int作为数据,而当您使用函数时,编译器将插入该函数指针地址,而不是存储在内存中。
所以
struct {
int i;
void func();
} test;
如果将对齐方式/装箱设置得很紧,那么sizeof(test)==sizeof(int)
应该为true。