一个 VLA (可变长度数组)的实现
VLA (可变长度数组) 是 C 语言在 C99 之后加入的一个很方便的语言特性,但是 MSVC 已经明确不支持 VLA 了。而且 Linux 的内核代码中曾经使用过 VLA ,而现在已经移除了 VLA 。看起来,VLA 带来的安全问题比它的便利性要多。
但是,日常用 C 语言做开发时,经常还是需要变长数组的。既然直接用 C 语言的 VLA 有诸多问题,那么还是需要额外实现一个比较好。C 没有 C++ 那样的模板支持,一般的通用 VLA 实现很难做到类型安全。即使用 C++ ,STL 中的 vector ,这个最常用的 VLA 实现,也不总是切合应用场景的。比如,std::vector 它的数据一般还是分配在堆上,而不是栈上。相比原生创建在栈上的数组,它可能性能有影响且有可能制造更多的堆内存碎片。
我认为一个通用的 VLA 库,应该做到:
强类型支持,且不需要每次调用相关 API 时都指定数据类型。
当我们在栈上使用 VLA 时,应该尽量接近原生数组的性能,尽可能的避免从堆上分配内存。
VLA 可以方便的在函数间传递引用。
VLA 的引用可以持久保持。
访问 VLA 的数据可以被 inline ,尽可能的避免额外的函数调用。
由于我的 C 代码大量的和 Lua 交互,如果 VLA 可以利用 Lua 来管理内存,而不直接使用 C 的内存堆,会是一个不错的加分项。这样、在 Lua 函数中临时使用 VLA 时,小块的内存便可直接在 C Stack 上分配;大块内存可以利用 Lua 的临时 userdata ,然后交给 Lua 的 GC 回收。我们在退出函数调用时,就不必显式的销毁 VLA 对象。
之前我写过好几版 VLA 库都不太满意。最近找到了一些技巧,重新实现了一版,以上的需求基本都满足了。
第一个棘手的问题是,C 语言如何做到通用且类型安全的 VLA ?我是这样解决的:
对于一个 VLA 对象,其实分两个部分,其一是用于数据访问的指针,我称之为访问器,其二是 VLA 本身的数据,包括实际数据和元数据。元数据一般有数组的大小、容量等,实际数据一般储存在连续内存空间中。
访问器本身需要契合 VLA 内部数据的类型。而 C 语言在泛型支持方面很弱,传统的方法是用 void * 来指代数据区,这就导致了很多 VLA 实现的 API 难以使用。
我的 VLA 实现引入了两个概念。对于 VLA 对象本身,我用一个抽象的 vla_handle_t
类型来保持引用,而访问器则选用 raw 指针,这个指针的类型直接是内部数据的类型。因为访问器本身可以以非常低廉的成本从 handle 构造出来,所以它并不需要持久保存。这就为宏技巧提供了空间。
所以,最终的 API 就是 vla_using(name, type, handle)
。这是一个宏,它的作用是在栈上创建出一个名为 name 的访问器,并通过 handle 关联相关的 VLA 对象。下面是一个简化的示意版本(实际因为需要实现更多功能,比这个复杂的多):
#define vla_using(name, type, handle) \ type * name; \ vla_handle_t * name##_ref_ = &handle; \ init_vla_accessor((void **)&name, name##_ref_);
在使用这个 api 时,栈实际多了两个变量。一个是用户指定名字的访问器,它就是一个原生指针,所以在访问数据时,和原生数组没有任何区别;同时,还在栈上记录了 VLA 对象本身的引用。当我们想对 VLA 对象操作时,访问它就可以了。比如,求 VLA size 的 API 也是一个宏:
#define vla_size(name) vla_size_(name##_ref_)
这个宏把指针访问器的操作转发到了对应的 VLA 对象 handle 上。
第二个问题是,如何兼顾堆和栈的内存使用。
如果只有一个 VLA 数据结构,当我们在栈上使用的时候,倾向于在数据结构中预留一块几百字节的临时空间。如果数据不超过这个空间,就不必申请堆内存。函数调用结束后,这些栈空间就立刻回收了。但如果我们需要持久引用 VLA 对象,这个预留的额外空间就显得太浪费了。当然可以显式的做两套实现,由使用的人根据场合分别用对应的方案。但还是需要把它们两者统一起来,这样才可以有一致的接口,当模块不关心细节时,可以一致的使用。
所以我选择用一个 handle 来表示不同实现的 VLA 。
第三个问题和 Lua 有关。在栈上的临时内存空间如果超出,我们就需要申请额外的内存维持更大体积的 VLA 。一般的实现中,这些额外的内存会从 C 的内存堆中分配。对应的,在函数返回时,需要释放这些内存。如果我们临时创建 lua 的 userdata 就没这个烦恼了。Lua 在退出 C 函数后,那些临时构造的 userdata 会失去引用,随后被 lua GC 回收。在 Lua 5.4 中,启用分代 GC 的话,这个回收非常及时。
所以,我们还需要第三种 VLA 实现:采用 Lua 管理内存。
Lua 的内存管理和传统 C 程序很不一样,它并不是配对的分配/释放调用,而是围绕引用进行的。我们在给 C 模块做 Lua binding 时也经常会遇到一个固定的 C Struct 中需要一个 VLA 对象的情况。这时,如果 VLA 也用 Lua 管理内存生命期的话,就不用给 C Struct 添加额外的 gc 元方法了,而只需要把 VLA 对象所使用的 userdata 通过 uservalue 引用在宿主对象上。
我们的 VLA 模块需要兼容这种用法。
建议继续学习:
- 前端要给力之:URL应该有多长? (阅读:7051)
- C语言结构体里的成员数组和指针 (阅读:4862)
- 为什么数组标号是从0开始的? (阅读:4953)
- 将数组定义为常量 (阅读:4570)
- Tips of Linux C programming (阅读:3977)
- xml转数组的方法 (阅读:3500)
- IE的Get请求(URL)的最大长度限制 (阅读:3340)
- javascript扩展Array(数组)类 (阅读:3241)
- 动态数组的 C 实现 (阅读:3116)
- php数组排序 (阅读:3018)
扫一扫订阅我的微信号:IT技术博客大学习
- 作者:云风 来源: 云风的 BLOG
- 标签: vla 数组 长度
- 发布时间:2022-07-24 20:47:15
- [41] 界面设计速成
- [35] Oracle MTS模式下 进程地址与会话信
- [33] 如何拿下简短的域名
- [32] IOS安全–浅谈关于IOS加固的几种方法
- [32] 视觉调整-设计师 vs. 逻辑
- [32] 程序员技术练级攻略
- [31] 图书馆的世界纪录
- [30] android 开发入门
- [30] 【社会化设计】自我(self)部分――欢迎区
- [27] 读书笔记-壹百度:百度十年千倍的29条法则