C++ 在结构中嵌入函数汇编代码

C++ 在结构中嵌入函数汇编代码,c++,c,c++11,assembly,inline-assembly,C++,C,C++11,Assembly,Inline Assembly,我有一个非常特殊的问题:在C/+++中是否都可以,因为我确信在两种语言中指定函数的位置的问题是相同的?为什么?我有一个非常大的函数指针列表,我想消除它们 目前,这看起来像是在用户的RAM中重复了一百万次: struct { int i; void(* funptr)(); } test; 因为我知道在大多数汇编语言中,函数只是goto指令,所以我有以下想法。是否可以优化上述结构,使其看起来像这样 struct { int i; // embed the asse

我有一个非常特殊的问题:在C/+++中是否都可以,因为我确信在两种语言中指定函数的位置的问题是相同的?为什么?我有一个非常大的函数指针列表,我想消除它们

目前,这看起来像是在用户的RAM中重复了一百万次:

struct {
    int i;
    void(* funptr)();
} test;
因为我知道在大多数汇编语言中,函数只是goto指令,所以我有以下想法。是否可以优化上述结构,使其看起来像这样

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;
最后,内存中的东西应该是这样的:一个包含任何值的int,后跟test2引用的函数的汇编代码。我应该能够这样调用这些函数:void*&pointertotherstruct+sizeofint

你可能会认为我用这种方式优化应用程序太疯狂了,我不能透露更多关于它功能的细节,但如果有人对如何解决这个问题有一些建议,我将不胜感激。 我不认为有一个标准的方法来实现这一点,所以任何通过内联汇编程序/其他疯狂的东西来实现这一点的黑客方法都是值得赞赏的

不,不是真的

指定函数位置的方法是使用函数指针,您已经在这样做了

您可以创建不同的类型,这些类型具有各自不同的成员函数,但随后您又回到了原始问题

在过去,我曾尝试将自动生成作为预构建步骤,使用Python生成一个带有长switch语句的函数,该语句将int I映射到普通函数调用。这将以分支为代价消除函数指针。我不记得在我的情况下它是否值得,即使我有,也不能告诉我们在你的情况下它是否值得

因为我知道在大多数汇编语言中,函数只是goto指令

嗯,也许比那复杂一点

你可能会认为我这样优化应用程序是疯了

也许吧。试图消除间接性本身并不是一件坏事,所以我不认为你试图改进这一点是错误的。我只是觉得你不一定能

但是如果有人有什么建议的话


哈哈

我不明白这个优化的目的是为了节省内存吗

我可能误解了这个问题,但是如果你只是用一个普通函数替换你的函数指针,那么你的结构将只包含int作为数据,当你获取它的地址时,编译器将插入函数指针,而不是存储在内存中

那就这么做吧

struct {
    int i;
    void func();
} test;  

如果将对齐/打包设置为紧密,那么sizeoftest==sizeofint应该为true。

您真正需要做的唯一一件事是让编译器知道您想要在结构中使用的函数指针的常量值。然后,编译器可能会/希望将该函数调用内联到通过该函数指针调用的任何位置:

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();
}
-优化后没有调用或jmp

目前尚不清楚int i的意义是什么。请注意,从技术上讲,代码并不直接在这里的i之后,但更不清楚的是,您希望struct的实例看起来是什么样的,是它们中的代码还是在某种程度上是静态的?我觉得在你这方面有一些误解,编译器实际上产生了什么。。。。但是考虑编译器内联可以帮助你的方法,你可能会找到你需要的解决方案。如果您担心内联后的可执行文件大小,请告诉编译器,它将在速度和大小之间折衷。

这听起来是一个糟糕的主意,原因很多,可能不会节省内存,并且会通过使用数据稀释L1I缓存和使用代码稀释L1D缓存而影响性能。如果修改或复制对象,情况会更糟:自修改代码暂停

但是,这在C99/C11中是可能的,在结构的末尾有一个灵活的数组成员,您可以将其转换为函数指针

编译时,编译器输出与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 type声明,编译器将在x86-64 SysV调用约定中首先将AL归零,以防它实际上是一个可变函数,因为它在寄存器中不传递FP args

您必须在内存中分配通常不可执行的对象,除非它们是静态存储的常量,例如使用gcc-zexecstack编译。或者在POSIX或Windows上使用自定义mmap/mprotect或VirtualAlloc/VirtualProtect

或者,如果您的对象都是静态分配的,则可以通过在每个对象之前添加一个int成员来调整编译器输出,从而将.text部分中的函数转换为对象。也许有一些.section和链接器技巧,也许还有一个链接器脚本,你甚至可以自动 吃了它

但是,除非它们都是相同长度的,例如使用类似字符码[60]的填充,否则这不会形成一个可以索引的数组,因此您需要某种方法来引用所有这些可变长度对象

如果在调用某个对象的函数之前修改该对象,可能会带来巨大的性能下降:在x86上,在刚写入的内存位置附近执行代码时,会出现自修改代码管道核

或者,如果在调用其函数x86 pipeline flush之前复制了一个对象,或者在其他ISA上,则需要手动刷新缓存,以使I-cache与D-cache同步,以便可以执行新写入的字节。但是您不能复制这些对象,因为它们的大小没有存储在任何地方。无法在机器代码中搜索ret指令,因为0xc3字节可能出现在不是x86指令开始的地方。或者在任何ISA上,函数都可能有多条ret指令。或者以jmp而不是ret tailcall结束。 存储大小将开始破坏保存大小的目的,在每个对象中至少消耗一个额外字节

在运行时向代码中写入对象,然后将其转换为函数指针,是ISO和C++中未定义的行为。在GNUC/C++上,确保调用它上的内置清除缓存来同步缓存或其他必要的东西。是的,即使在x86上也需要这样做才能禁用死区消除优化:。在x86上,它只是编译时的东西,没有额外的asm。它实际上并没有清除任何缓存

如果您在运行时启动时复制,可能会分配一大块内存,并在复制时划出可变长度的内存块。如果分别对每一个进行malloc,则会浪费内存管理开销

这个想法不会节省内存,除非你有和你有对象一样多的函数 通常,实际函数的数量相当有限,许多对象都有相同函数指针的副本。您有一种手动滚动的C++虚拟函数,但是只有一个函数,您只需有一个函数指针,而不是一个VTABLE指针,指向该类类型的指针表。减少了一层间接寻址,显然您没有将对象自己的地址传递给函数

此级别间接寻址的几个好处之一是,一个指针通常比函数的整个代码小得多。如果不是这样,你的函数就必须很小

示例:有10个不同的函数,每个函数32字节,1000个对象带有函数指针,您总共有320字节的代码在I-cache中保持热状态,还有8000字节的函数指针。在您的对象中,每个对象又浪费了4个字节来对齐指针,使每个对象的总大小从12个字节变为16个字节。总之,整个structs+代码总共是16320字节。如果单独分配每个对象,则每个对象都有簿记

将机器代码内联到每个对象中,没有填充,即1000*4+32=36000字节,是总大小的两倍多

x86-64可能是最好的情况,其中指针为8字节,x86-64机器代码使用著名的复杂变长指令编码,这在某些情况下允许高代码密度,特别是在优化代码大小时。e、 g.代码高尔夫。但是,除非你的函数大部分是像leaeax这样的琐碎的东西,[rdi+rdi*2]3个字节=操作码+ModRM+SIB/ret 1个字节,否则它们仍然需要8个字节以上。这就是返回x*3;对于在x86-64系统V ABI中采用32位整数x arg的函数

如果它们是较大函数的包装器,则正常的调用rel32指令为5字节。对于RIP相对寻址模式,静态数据的加载至少为6字节opcode+modrm+rel32,或者专门加载EAX可以对绝对地址使用特殊的no modrm编码。但在x86-64中,这是64位绝对值,除非您也使用地址大小前缀,否则可能会导致Intel上的解码器出现LCP暂停。mov eax,[32位绝对地址]=addr32 0x67+opcode+abs32=6字节,所以这对任何人都没有好处

您的函数指针类型没有任何ARG,假设这是C++,其中FO表示声明中的FoWess,不像旧C,其中空的ARG列表与…有点类似。因此,我们可以假设您没有传递参数,因此要做任何有用的事情,函数可能会访问一些静态数据或进行另一个调用

更有意义的想法: 使用类似ILP32的ABI,CPU在64位模式下运行,但代码使用32位指针。这将使每个对象只有8个字节,而不是16个字节。一般来说,避免指针膨胀是x32或ILP32 ABI的经典用例

或者把你的代码编译成32位。但是,您有一些过时的32位调用约定,这些约定在堆栈上传递参数,而不是在寄存器上传递参数,并且不到regi的一半 对于位置无关的代码来说,更高的开销。没有EIP/RIP相对寻址

将无符号int表索引存储到函数指针表中。如果有100个函数,但有10k个对象,那么表的长度只有100个指针。在ASM中,如果所有功能都被填充到相同的长度,则可以索引一个直接计算Goto风格的代码数组,但是C++中不能这样做。使用函数指针表进行额外的间接寻址可能是最好的选择

e、 g

叮当声/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]


与在每个对象中存储函数指针相比,这是一个额外的间接寻址级别,但它允许您将每个对象从16个字节缩减到8个字节,就像使用32位指针一样。或者5或6个字节,如果您使用无符号short或uint8,并将结构与GNU C中的uu属性u打包。

虽然有一些技巧允许将代码嵌入到数据中,但我不认为您会从中受益。最终目标是什么?因为我知道在大多数汇编语言中,函数只是goto指令,你从哪里知道的?goto只是一个jmp语句,而call不仅仅是这样。如果只是简单的跳转,递归就不可能了。@thejack你的函数代码比指针小吗???因为如果不是,那么通过在结构中存储函数代码,您将使用更多的内存,而不是更少的内存。@thejack段对内存使用有什么影响?你的架构是什么?还要注意void*&pointertotherstruct+sizeofint;这绝对是错误的。您将移动4个指针大小,超过存储pointerToTheStruct的位置,然后调用该位置。我们明白你的意思,但很明显,这不是实际的要求。对,但现在你必须在程序中选择这种类型的变体,这意味着某种查找,这是你刚刚通过删除函数指针设法避免的!在某种程度上,计算机必须决定调用哪个函数,而你无法摆脱这个要求:你只能决定最便宜的实现方法。@LightnessRacesinOrbit是真的,有人可能会认为函数指针指向不同的位置,但我看不出这个问题明确地提到了这一点,他的jmp_开始示例也没有指出这一点。我想我确实假设了这一点,但考虑到上下文、大量函数指针列表、整数索引的存在……我认为这是合理的。。。
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] ();
}
 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