【问题标题】:How to dynamically load often re-generated c code quickly?如何快速动态加载经常重新生成的c代码?
【发布时间】:2012-09-01 09:49:46
【问题描述】:

我希望能够动态生成 C 代码并将其快速重新加载到我正在运行的 C 程序中。

我在 Linux 上,怎么会这样?

Linux 上的库 .so 文件可以在运行时重新编译和重新加载吗?

是否可以在不生成 .so 文件的情况下对其进行编译,编译后的输出能否以某种方式进入内存然后重新加载?我想快速重新加载编译后的代码。

【问题讨论】:

  • 你确定时间有这么大的限制! (你测量过吗)?
  • 我很想知道你为什么问这个问题以及你在编写什么样的应用程序!
  • 我正在用 Goto 语句和全局变量编写一种语言。我相信函数隐藏了函数的实现细节(封装),而OO封装了数据。我反对隐藏任何东西,并认为人们在“goto is evil”论文之后没有足够努力地使用 Goto 和全局变量。无论如何,我不会在这里争论这一点了!哈哈……有点疯狂。所以我选择 LLVM,因为我需要 goto,而 C/C++ 不再允许 goto(标签除外)。

标签: c linux


【解决方案1】:

您想要做的事情是合理的,而我正是在 MELT(一种用于扩展 GCC 的高级域特定语言;MELT 编译为 C,通过一个用 MELT 编写的翻译器本身)。

首先,在生成 C 代码(或许多其他源语言)时,一个好的建议是在内存中保留某种 abstract syntax tree (AST)。因此,首先构建生成的 C 代码的整个 AST,然后将其作为 C 语法发出。不要认为你的代码生成框架没有明确的 AST(换句话说,用一堆 printf 生成 C 代码是一场维护噩梦,你想要一些中间表示)。

其次,生成 C 代码的主要原因是利用良好的优化编译器(另一个原因是 C 的可移植性和普遍性)。如果您不关心生成代码的性能(并且 TCC 非常快地将 C 编译成非常幼稚和缓慢的机器代码),您可以使用其他一些方法,例如使用一些 JIT 库,如 Gnu lightning(非常快速地生成慢速机器代码)、Gnu LibjitASMJIT(生成的机器代码更好一点)、LLVMGCCJIT(生成良好的机器代码,但生成时间与编译器相当)。

因此,如果您生成 C 代码并希望它快速运行,则 C 代码的编译时间不可忽略(因为您可能会 fork 一个 gcc -O -fPIC -shared 命令从生成的 @ 中生成一些共享对象 foo.so 987654335@)。根据经验,生成 C 代码比编​​译它(使用gcc -O)花费的时间要少得多。在 MELT 中,C 代码的生成速度比 GCC 编译快 10 倍以上(通常快 30 倍)。但是 C 编译器所做的优化是值得的。

一旦你发布了你的 C 代码,将它的编译分叉成一个.so 共享对象,你就可以dlopen 它。不要害羞,我的manydl.c 示例演示了在 Linux 上您可以 dlopen 大量共享对象(数十万个)。真正的瓶颈是生成的 C 代码的编译。在实践中,在 Linux 上您实际上并不需要 dlclose(除非您正在编写需要运行数月的服务器程序);一个未使用的共享模块实际上可以保留dlopen-ed,并且您主要是在泄漏进程地址空间(这是一种廉价资源),因为大部分未使用的.so 将被换出。 dlopen 做的很快,需要时间的是编译一个 C 源代码,因为你真的希望由 C 编译器完成优化。

您可以使用许多其他不同的方法,例如有一个字节码解释器并为该字节码生成,使用 Common Lisp(例如 Linux 上的 SBCL,它可以动态编译成机器码)、LuaJit、Java、MetaOcaml 等。

正如其他人所建议的,您不太关心编写 C 文件的时间,实际上它会保留在文件系统缓存中(另请参阅 this)。而且编写它比编译它要快得多,所以留在内存中是不值得的。如果您担心 I/O 时间,请使用一些 tmpfs

附录

你问了

Linux 上的库.so 文件可以在运行时重新编译和重新- 加载吗?

当然可以:你应该派生一个命令来从生成的 C 代码构建库(例如 gcc -O -fPIC -shared generated.c -o generated.so,但你可以间接地做到这一点,例如通过运行 make -j,特别是如果 generated.so 很大足以将generated.c拆分成几个C生成的文件!)然后你用dlopen动态加载你的库(给出像/some/file/path/to/generated.so这样的完整路径,可能还有RTLD_NOW标志)你必须使用dlsym 在里面找到相关的符号。不要想重新-加载(第二次)相同的generated.so,最好发出一个唯一的generated1.c(然后是generated2.c等...)C文件,然后编译它到一个独特 generated1.so(第二次到generated2.so,等等......)然后到dlopen它(这可以完成数十万次)。您可能希望在发出的generated*.c 文件中拥有一些constructor 函数,这些函数将在dlopengenerated*.so 时间执行

您的基础应用程序应该已经定义了关于dlsym-ed 名称集(通常是函数)以及如何调用它们的约定。它应该只直接调用您的 generated*.sodlsym-ed 函数指针中的函数。例如,在实践中,您会决定每个generated*.c 定义一个函数void dynfoo(int)int dynbar(int,int),并将dlsym"dynfoo""dynbar" 一起使用,并调用这些直通函数指针(由dlsym 返回)。您还应该定义如何以及何时调用这些dynfoodynbar 的约定。您最好将您的基础应用程序与-rdynamic 链接起来,以便您的generated*.c 文件可以调用您的应用程序函数。

希望您的generated*.so重新定义 现有名称。例如,您不想在 generated*.c 中重新定义 malloc 并期望所有堆分配函数都能神奇地使用您的新变体(这可能行不通,即使这样做也会很危险) .

您可能不会打扰dlclose 动态加载的共享对象,除非在应用程序清理和退出时(但我根本不会打扰dlclose)。如果您执行dlclose 一些动态加载的generated*.so 文件,请确保其中没有使用任何内容:没有指针,甚至调用帧中的返回地址都不存在。

附: MELT 翻译器目前将 57KLOC 的 MELT 代码翻译成近 1770KLOC 的 C 代码。

【讨论】:

  • 这就是吸引人的地方,C 编译器可以生成好的机器代码,而且 C 是众所周知的语言并且易于理解。学习 LLVM 代码需要付出一些努力,这对我来说可能是太多的努力。其他选项不会生成有效的代码。 (效率不高)。
  • 这是最好的答案,但最后我选择使用 LLVM,因为我需要一些普通 C++ 不提供的功能,即 GOTO 语句到任意位置(即使那是邪恶的)。这仍然是我认为的问题的最佳答案。
【解决方案2】:

如果你想动态地重新加载一个库,你可以使用dlopen函数(见mans)。它会打开一个库 .so 文件并返回一个指向它的 void* 指针,然后您可以使用 dlsym 获取指向库的任何函数/变量的指针。

要在内存中编译你的库,我认为你能做的最好的事情就是按照here 的描述创建内存文件系统。

【讨论】:

  • 它是dlopendlsym(不是dl_open ...)!
  • 非常感谢,我总是犯这个错误:(
【解决方案3】:

你确定 C 是正确的答案吗?有各种解释语言,例如 Lua、Bigloo Scheme,甚至可能是 Python,它们都很好地嵌入到了现有的 C 应用程序中。您可以使用扩展语言编写动态部分,这将支持在运行时重新加载代码。

明显的缺点是性能 - 如果您绝对需要编译 C 的原始速度,那么这些可能是不行的。

【讨论】:

    【解决方案4】:

    您最好的选择可能是TCC 编译器,它可以让您完全做到这一点 --- 编译源代码、将其添加到您的程序、运行它,所有这些都无需接触文件。

    要获得更强大但非基于 C 的解决方案,您可能应该查看LLVM 项目,它的作用大致相同,但从生成 JIT 的角度来看。您不必通过 C 语言,而是使用一种抽象的可移植机器代码,但生成的代码加载速度更快,并且正在更积极的开发中。

    OTOH,如果您想手动完成所有操作,只需使用 gcc,编译 .so 然后自己加载它,dlopen()dlclose() 将满足您的需求。

    【讨论】:

    • TCC 看起来令人鼓舞,虽然我在网站上看到了这个注释:Note: I am no longer working on TCC. Check the mailing list to get up to date information.。一方面,我在他们的 git repo 中看到了许多最近的变化。但另一方面,最后一次发布是在 3 年前。不太清楚这会把事情留在哪里......
    • 邮件列表仍然有效,但 Fabrice Bellard 现在正在处理其他事情。嗯...值得指出的是,tcc 确实是作为混淆 C 竞赛参赛者开始的。这是一个伟大的黑客,并且做得很好,但它并不漂亮。几年前我用它在一个不支持加载可执行代码的平台上加载可执行代码取得了巨大成功。
    • 但是TCC确实有一个弱点:虽然它的编译时间不错,但是生成的机器码却很慢。 (TCC 将非常快的 C 代码编译成慢速机器代码)。如果生成了 C 代码,您可能会遵循一些不同的路线......
    • 因此,如果您关心动态加载的代码以快速运行,TCC 不适合您,因为 TCC 生成的代码很慢(有时比 gcc -O 生成的代码慢 2 到 3 倍)
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-02-17
    • 1970-01-01
    • 2018-07-28
    • 2016-09-03
    相关资源
    最近更新 更多