技术头条 - 一个快速在微博传播文章的方式     搜索本站
您现在的位置首页 --> 算法 --> 一个 VLA (可变长度数组)的实现

一个 VLA (可变长度数组)的实现

浏览:860次  出处信息

   VLA (可变长度数组) 是 C 语言在 C99 之后加入的一个很方便的语言特性,但是 MSVC 已经明确不支持 VLA 了。而且 Linux 的内核代码中曾经使用过 VLA ,而现在已经移除了 VLA 。看起来,VLA 带来的安全问题比它的便利性要多。

   但是,日常用 C 语言做开发时,经常还是需要变长数组的。既然直接用 C 语言的 VLA 有诸多问题,那么还是需要额外实现一个比较好。C 没有 C++ 那样的模板支持,一般的通用 VLA 实现很难做到类型安全。即使用 C++ ,STL 中的 vector ,这个最常用的 VLA 实现,也不总是切合应用场景的。比如,std::vector 它的数据一般还是分配在堆上,而不是栈上。相比原生创建在栈上的数组,它可能性能有影响且有可能制造更多的堆内存碎片。

   我认为一个通用的 VLA 库,应该做到:

  1. 强类型支持,且不需要每次调用相关 API 时都指定数据类型。

  2. 当我们在栈上使用 VLA 时,应该尽量接近原生数组的性能,尽可能的避免从堆上分配内存。

  3. VLA 可以方便的在函数间传递引用。

  4. VLA 的引用可以持久保持。

  5. 访问 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 模块需要兼容这种用法。

   综上所述,我初步实现了一版

建议继续学习:

  1. 前端要给力之:URL应该有多长?    (阅读:6901)
  2. C语言结构体里的成员数组和指针    (阅读:4589)
  3. 为什么数组标号是从0开始的?    (阅读:4593)
  4. 将数组定义为常量    (阅读:4439)
  5. Tips of Linux C programming    (阅读:3754)
  6. xml转数组的方法    (阅读:3271)
  7. IE的Get请求(URL)的最大长度限制    (阅读:3187)
  8. javascript扩展Array(数组)类    (阅读:3093)
  9. 动态数组的 C 实现    (阅读:2962)
  10. php数组排序    (阅读:2871)
QQ技术交流群:445447336,欢迎加入!
扫一扫订阅我的微信号:IT技术博客大学习
© 2009 - 2024 by blogread.cn 微博:@IT技术博客大学习

京ICP备15002552号-1