Hacker News 中文摘要

RSS订阅

C语言中闭包的成本 -- The Cost of a Closure in C

文章摘要

这篇文章探讨了C语言中闭包(closure)的性能成本问题。作者指出,当前C和C++扩展中闭包的设计方案存在诸多性能缺陷。闭包是一种包含额外数据的编程语言构造,可以视为函数概念的泛化,但作者认为现有实现方式不够优化,带来了不必要的性能开销。

文章总结

C语言中闭包的性能成本分析

背景与问题

闭包(Closures)是编程语言中一种将数据与指令绑定的结构,允许函数访问非直接输入(参数)或返回值的外部数据。尽管现代语言普遍支持闭包,但C和C++的闭包实现方案在性能和设计上存在显著不足。

闭包的核心问题

以C标准库的qsort为例,传统方法通过静态变量(static)传递额外数据存在以下问题: - 无法创建多份独立副本,导致状态管理复杂; - 多线程环境下易引发竞争条件; - 无法直接引用局部数据(如数组本身)。

现有解决方案

  1. 修改函数签名:如BSD的qsort_r或C11 Annex K的qsort_s,通过void*传递用户数据。
  2. GNU嵌套函数:直接引用外部变量,但依赖可执行栈,存在安全和性能隐患。
  3. Apple Blocks:通过__block修饰符捕获变量,但依赖运行时内存管理。
  4. C++ Lambda:结合模板和类型擦除(如std::function),灵活性高但可能引入额外开销。

性能测试:Knuth的Man-or-Boy测试

通过递归深度测试不同闭包方案的性能,结果如下(基于Apple Clang 17和GCC 15): - 最优方案:未类型擦除的C++ Lambda,编译器可深度优化。 - 次优方案:轻量级类型擦除(如std::function_ref),性能接近手写C++类。 - 最差方案:GNU嵌套函数(因可执行栈限制优化)和std::function(堆分配成本高)。

关键发现

  1. 编译器信息越充分,性能越好:Lambda的模板实例化允许内联和优化。
  2. 类型擦除的成本可控std::function_ref等非分配型擦除器性能损失较小。
  3. 现有扩展的缺陷
    • GNU嵌套函数因栈执行限制拖累优化;
    • Apple Blocks的ARC机制导致固定开销。

未来方向

  • 宽函数指针:提议在C中引入类似{函数指针, 上下文}的低成本闭包类型。
  • 标准化需求:当前方案(如GNU嵌套函数或Apple Blocks)均未达到理想性能,需语言级支持。

结论

闭包设计需平衡灵活性与性能。C++ Lambda展示了静态优化的潜力,而C的扩展方案(如GNU嵌套函数)因实现限制表现不佳。未来C标准应考虑引入零成本抽象的闭包机制,同时兼容现有扩展。

(注:原文中的代码示例、图表及详细数据可参考原始链接

评论总结

以下是评论内容的总结:

  1. 对C语言扩展功能的看法

    • 有开发者喜欢使用GNU扩展中的蹦床函数(评论1:"I actually enjoy trampoline functions in C a bit")
    • 有建议为C语言添加"state"关键字来管理函数状态(评论2:"having a 'state' keyword for declaring variables in a 'stateful' function")
  2. 关于lambda实现和性能的讨论

    • 认为测试结果受内联设置影响,lambda选项在无内存分配时性能相当(评论3:"all lambda options except for the one involving allocation are equivalent modulo inlining")
    • 指出C++ lambda的优势在于编译器可以优化静态链接(评论8:"the modern C++ 'Lambda' approach...is effectively a compile-time calculated static link")
  3. 对C语言改进的建议

    • 呼吁C标准应包含完整的函数指针和闭包支持(评论4:"include a straightforward, first class wide function pointer along with a closure story")
    • 认为类似C++引用捕获的局部函数扩展最适合C语言(评论7:"local functions...that behave like C++ byref(&) capturing lambdas makes the most sense for C")
  4. 代码风格讨论

    • 对参数解析代码风格提出简化建议(评论5:"Why not if (argc > 1 && strcmp(argv[1], "-r") == 0)")
  5. 语言选择考量

    • 有开发者因lambda和RAII特性选择C++而非Rust(评论6:"using C++...specifically for the lambdas and RAII")
  6. 对文章内容的指正

    • 指出作者对static关键字的理解有误(评论10:"in this usage...'static' has nothing to do with persistence")
  7. 其他

    • 简单表达对C++的支持(评论9:"c++ for the win!!")
    • 发现文章中的拼写错误(评论11:"the only typo is...the word son")

关键引用保留: - 评论3:"all lambda options except for the one involving allocation are equivalent modulo inlining"("除了涉及内存分配的选项外,所有lambda实现在内联方面都是等效的") - 评论8:"the modern C++ 'Lambda' approach...is effectively a compile-time calculated static link"("现代C++的Lambda方法...本质上是编译时计算的静态链接") - 评论10:"in this usage...'static' has nothing to do with persistence"("在这种用法中...'static'与持久性无关")