我想通过我在项目中遇到的实际问题来解释我的问题。
我正在编写一个c库(其行为类似于可编程vi editor
),我计划提供一系列API(总共超过20个):
void vi_dw(struct vi *vi);
void vi_de(struct vi *vi);
void vi_d0(struct vi *vi);
void vi_d$(struct vi *vi);
...
void vi_df(struct vi *, char target);
void vi_dd(struct vi *vi);
这些API不执行核心操作,它们只是包装器。例如,我可以像这样实现vi_de()
:
void vi_de(struct vi *vi){
vi_v(vi); //enter visual mode
vi_e(vi); //press key 'e'
vi_d(vi); //press key 'd'
}
但是,如果包装器就这么简单,我必须编写20多个类似的包装函数
因此,我考虑实施更复杂的包装来减少数量:
void vi_d_move(struct vi *vi, vi_move_func_t move){
vi_v(vi);
move(vi);
vi_d(vi);
}
static inline void vi_dw(struct vi *vi){
vi_d_move(vi, vi_w);
}
static inline void vi_de(struct vi *vi){
vi_d_move(vi, vi_e);
}
...
函数vi_d_move()
是一个更好的包装函数,他可以将类似移动操作的一部分转换为API,但不是全部,例如vi_f()
,它需要另一个带有第三个参数的包装器{{1 }。。
我完成了从我的项目中挑选的示例。
上面的伪代码比实际情况简单,但足以表明:
包装器越复杂,我们需要的包装器越少,它们就越慢。(它们将变得更加间接或需要考虑更多条件)。
有两个极端:
只使用一个包装器,但复杂程度足以采用所有移动操作并将它们转换为相应的API。
使用超过二十个小而简单的包装。一个包装器是一个API。
对于案例1,包装器本身很慢,但它更有可能驻留在缓存中,因为它经常被执行(所有API共享它)。这是一条缓慢但热门的道路。
对于案例2,这些包装器简单快速,但在缓存中驻留的机会较少。至少,对于第一次调用的API,将发生缓存未命中(CPU需要从内存中获取指令,但不需要L1,L2)。
目前,我实现了五个包装器,每个包装器都相对简单快速。这似乎是一种平衡,但似乎只是。我选择五只是因为我觉得移动操作可以自然地分为五组。我不知道如何评估它,我不是指一个分析器,我的意思是,理论上,在这种情况下应该考虑哪些主要因素?
在后期,我想为这些API添加更多细节:
这些API需要快速。因为此库设计为高性能虚拟编辑器。删除/复制/粘贴操作旨在接近裸C代码。
基于此库的用户程序很少调用所有这些API,只调用其中的一部分,每个API通常不超过10次。
在实际情况中,这些简单包装器的大小各约为80个字节,即使合并为单个复杂的包装也不会超过160个字节。 (但会引入更多if-else分支)。
4,与使用库的情况一样,我将以char target
为例(稍微偏离主题,但有些朋友想知道我为什么如此关心其性能):
lua-shell
是一个* nix shell,它使用lua-shell
作为脚本。它的命令执行单元(执行forks(),execute()..)只是一个注册到lua状态机的C模块。
lua
将所有内容视为Lua-shell
。
所以,当用户输入时:
lua
然后按local files = `ls -la`
。字符串输入首先发送到lua-shell的预处理器----将混合语法转换为纯lua代码:
Enter
local file = run_command("ls -la")
是lua-shell命令执行单元的入口,我之前说过,它是一个C模块。
我们现在可以谈谈run_command()
。 lua-shell的预处理器是我写的库的第一个用户。这是它的相对代码(伪):
libvi
上面的代码是luashell预处理器实现的一部分。 生成纯lua代码后,他将其提供给Lua State Machine并运行它。
shell用户对#include"vi.h"
vi_loadstr("local files = `ls -la`");
vi_f(vi, '`');
vi_x(vi);
vi_i(vi, "run_command(\"");
vi_f(vi, '`');
vi_x(vi);
vi_a(" \") ");
和新提示之间的时间间隔很敏感,在大多数情况下,lua-shell需要具有更大尺寸和更复杂混合语法的预处理脚本。
这是使用Enter
的典型情况。
答案 0 :(得分:12)
我不会那么关心缓存未命中(特别是在你的情况下),除非你的基准(启用了编译器优化,即使用gcc -O2 -mtune=native
进行编译如果使用{{3} } ....)表明它们很重要。
如果性能非常重要,请启用更多优化(可能使用链接时优化的gcc -flto -O2 -mtune=native
编译和链接整个应用程序或库),并仅手动优化至关重要。您应该信任您的GCC 。
如果您处于设计阶段,请考虑使您的应用程序多线程或以某种方式并发和并行。小心,这可以比缓存优化更快地加速它。
目前还不清楚您的图书馆是什么以及您的设计目标是什么。增加灵活性的可能性可能是在应用程序中嵌入了一些解释器(如optimizing compiler或lua或guile等),因此可以通过脚本进行配置。在许多情况下,这种嵌入可能足够快(特别是当应用程序特定的基元具有足够高的水平时)。另一种(更复杂的)可能性是通过某些python库提供metaprogramming能力,例如JIT compiling或libjit(因此您可以将用户脚本“编译”成动态制作机器代码)。
顺便说一句,你的问题似乎集中在指令缓存未命中。我认为数据缓存未命中更重要(并且编译器不太可以优化),这就是为什么你更喜欢例如链接列表的向量(更一般地关注低级数据结构,侧重于使用顺序 - 或缓存友好 - 访问)(您可以通过Herb Sutter找到一个很好的视频来解释最后一点;我忘记了参考资料)
在某些非常具体的情况下,使用最近的libgccjit或GCC,添加一些Clang可能稍微提高性能(通过减少缓存未命中),但它也可能会对它造成很大的伤害,所以我一般不推荐使用它,但请参阅__builtin_prefetch
。