【发布时间】:2017-07-05 21:48:34
【问题描述】:
我想通过我在项目中遇到的一个实际问题来解释我的问题。
我正在编写一个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(),这需要另一个带有第三个参数char target的包装器。
我完成了从我的项目中挑选的示例的解释。
上面的伪代码比实际情况简单,但足以说明:
包装器越复杂,我们需要的包装器就越少,它们的运行速度也会越慢。(它们会变得更加间接或需要考虑更多的条件)。
有两个极端:
只使用一个包装器,但足够复杂,可以采用所有移动操作并将它们转换为相应的 API。
使用二十多个小而简单的包装器。一个包装器就是一个 API。
对于案例 1,包装器本身很慢,但它有更多机会驻留在缓存中,因为它经常被执行(所有 API 共享它)。这是一条缓慢但炙手可热的道路。
对于情况 2,这些包装器简单快速,但驻留在缓存中的机会较小。至少,对于任何第一次调用的 API,都会发生缓存未命中。(CPU 需要从内存中获取指令,而不是 L1、L2)。
目前,我实现了五个包装器,每个都比较简单和快速。这似乎是一种平衡,但似乎只是。我之所以选择五,是因为我觉得移动操作自然可以分为五组。我不知道如何评估它,我不是说分析器,我的意思是,理论上,在这种情况下应该考虑哪些主要因素?
在后期,我想为这些 API 添加更多细节:
这些 API 需要快速。因为这个库被设计成一个高性能的虚拟编辑器。删除/复制/粘贴操作旨在接近裸 C 代码。
基于此库的用户程序很少调用所有这些 API,仅调用其中的一部分,而且通常每个调用不超过 10 次。
在实际情况下,这些简单包装器的大小约为 80 字节,即使合并成一个复杂的包装器也不超过 160 字节。 (但会引入更多 if-else 分支)。
4、以库的使用情况为例,我以lua-shell为例(有点跑题,但有些朋友想知道我为什么这么在意它的性能):
lua-shell 是一个 *nix shell,它使用 lua 作为其脚本。它的命令执行单元(执行 forks()、execute()..)只是一个注册到 lua 状态机的 C 模块。
Lua-shell 将所有内容视为lua。
所以,当用户输入时:
local files = `ls -la`
然后按Enter。输入的字符串首先发送到 lua-shell 的预处理器——将混合语法转换为纯 lua 代码:
local file = run_command("ls -la")
run_command()是lua-shell命令执行单元的入口,我之前说过,是一个C模块。
我们现在可以谈谈libvi。 lua-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(" \") ");
上面的代码是 luashell 的预处理器实现的一部分。 生成纯 lua 代码后,将其提供给 Lua 状态机并运行。
shell 用户对Enter 和新提示之间的时间间隔很敏感,在大多数情况下 lua-shell 需要更大尺寸和更复杂的混合语法的预处理脚本。
这是使用libvi 的典型情况。
【问题讨论】:
-
你是在编写一个真正的source code editor(那么性能就没有那么重要了)还是你的库真正在做什么?为什么它对性能如此重要?您是否对其进行基准测试(在编译时启用优化)并对其进行分析?请编辑您的问题以解释更多真正的动机以及您想到的应用程序。
-
以最易于理解和易于维护的方式组织您的代码。除非您的代码非常频繁地执行,例如每秒 60 次,因为它必须为每个图形帧完成,通过优化指令缓存在程序的整个生命周期中节省的时间可能总计少于您的时间花费在优化和后续额外的维护时间上。例如,如果您通过优化一个小时来缩短一微秒,那么您的代码必须执行 36 亿次才能恢复该小时。
-
@JeremyP 所说的,实际上比这更糟。为了避免优化尝试在未来产生负面影响,您可能必须重新测试每个新版本的编译器、操作系统、硬件环境等,因此 Jeremy 的设计/测试/调试/测量示例“标准时间”有不断重复:(
-
为什么标记为 C++?
void vi_dw(struct vi *vi);显然是 C,在 C++ 中应该是void vi::dw( )。 -
不要垃圾标签! C++ 不是 C。