Linux 下 C/POSIX 标准库实现对比
Linux 下 C/POSIX 标准库实现对比
来自 Eta Labs 的一个项目。 下表和后续的注释对比了 Linux 下一些不同的标准库实现,特别关注功能丰富性和臃肿之间的平衡。我尽量做到公平和客观,但由于我是 musl 的作者,这可能会影响我对比较方面的选择。 未来这个对比的方向包括详细的性能基准测试,以及加入更多的库实现,特别是 Google 的 Bionic 和其他 BSD libc 移植。
臃肿程度对比 | musl | uClibc | dietlibc | glibc ---|---|---|---|--- 完整的 .a 文件集 | 426k | 500k | 120k | 2.0M † 完整的 .so 文件集 | 527k | 560k | 185k | 7.9M † 最小的静态 C 程序 | 1.8k | 5k | 0.2k | 662k 静态 hello (使用 printf) | 13k | 70k | 6k | 662k 动态开销 (最小脏页) | 20k | 40k | 40k | 48k 静态开销 (最小脏页) | 8k | 12k | 8k | 28k 静态 stdio 开销 (最小脏页) | 8k | 24k | 16k | 36k 可配置功能集 | 否 | 是 | 最小 | 最小
资源耗尽时的行为 | musl | uClibc | dietlibc | glibc ---|---|---|---|--- 线程局部存储 | 报告失败 | 中止 | n/a | 中止 SIGEV_THREAD 定时器 | 不失败 | n/a | n/a | 丢失溢出 pthread_cancel | 不失败 | 中止 | n/a | 中止 regcomp 和 regexec | 报告失败 | 崩溃 | 报告失败 | 崩溃 fnmatch | 不失败 | 未知 | 不失败 | 报告失败 printf 系列 | 不失败 | 不失败 | 不失败 | 报告失败 strtol 系列 | 不失败 | 不失败 | 不失败 | 不失败
性能对比 | musl | uClibc | dietlibc | glibc ---|---|---|---|--- 小块内存分配 & 释放 | 0.005 | 0.004 | 0.013 | 0.002 大块内存分配 & 释放 | 0.027 | 0.018 | 0.023 | 0.016 内存分配竞争, 本地 | 0.048 | 0.134 | 0.393 | 0.041 内存分配竞争, 共享 | 0.050 | 0.132 | 0.394 | 0.062 零填充 (memset) | 0.023 | 0.048 | 0.055 | 0.012 字符串长度 (strlen) | 0.081 | 0.098 | 0.161 | 0.048 字节搜索 (strchr) | 0.142 | 0.243 | 0.198 | 0.028 子字符串 (strstr) | 0.057 | 1.273 | 1.030 | 0.088 线程创建/加入 | 0.248 | 0.126 | 45.761 | 0.142 互斥锁锁定/解锁 | 0.042 | 0.055 | 0.785 | 0.046 UTF-8 解码 (缓冲) | 0.073 | 0.140 | 0.257 | 0.351 UTF-8 解码 (逐字节) | 0.153 | 0.395 | 0.236 | 0.563 Stdio putc/getc | 0.270 | 0.808 | 7.791 | 0.497 Stdio putc/getc (未锁定) | 0.200 | 0.282 | 0.269 | 0.144 正则表达式编译 | 0.058 | 0.041 | 0.014 | 0.039 正则表达式搜索 (a{25}b) | 0.188 | 0.188 | 0.967 | 0.137 自执行 (静态链接) | 234µs | 245µs | 272µs | 457µs 自执行 (动态链接) | 446µs | 590µs | 675µs | 864µs
ABI 和版本控制对比 | musl | uClibc | dietlibc | glibc ---|---|---|---|--- 稳定的 ABI | 是 | 否 | 非官方 | 是 LSB 兼容的 ABI | 不完整 | 否 | 否 | 是 向后兼容性 | 是 | 否 | 非官方 | 是 向前兼容性 | 是 | 否 | 非官方 | 否 原子升级 | 是 | 否 | 否 | 否 符号版本控制 | 否 | 否 | 否 | 是
算法对比 | musl | uClibc | dietlibc | glibc ---|---|---|---|--- 子字符串搜索 (strstr) | twoway | naive | naive | twoway 正则表达式 | dfa | dfa | backtracking | dfa 排序 (qsort) | smoothsort | shellsort | naive quicksort | introsort 分配器 (malloc) | musl-native | dlmalloc | diet-native | ptmalloc
特性对比 | musl | uClibc | dietlibc | glibc ---|---|---|---|--- 符合标准的 printf | 是 | 是 | 否 | 是 精确的浮点数打印 | 是 | 否 | 否 | 是 C99 数学库 | 是 | 部分 | 否 | 是 C11 线程 API | 是 | 否 | 否 | 否 C11 线程局部存储 | 是 | 是 | 否 | 是 GCC libstdc++ 兼容性 | 是 | 是 | 否 | 是 POSIX 线程 | 是 | 是, 大部分架构 | 损坏 | 是 POSIX 进程调度 | stub | 不正确 | 否 | 不正确 POSIX 线程优先级调度 | 是 | 是 | 否 | 是 POSIX localedef | 否 | 否 | 否 | 是 宽字符接口 | 是 | 是 | 最小 | 是 遗留 8 位代码页 | 否 | 是 | 最小 | 慢, 通过 gconv 遗留 CJK 编码 | 否 | 否 | 否 | 慢, 通过 gconv UTF-8 多字节 | native; 100% 符合标准 | native; 不符合标准 | 危险地不符合标准 | 慢, 通过 gconv; 不符合标准 Iconv 字符转换 | 大部分主流编码 | 主要是 UTFs | 否 | the kitchen sink Iconv 音译扩展 | 否 | 否 | 否 | 是 Openwall-style TCB shadow | 是 | 否 | 否 | 否 Sun RPC, NIS | 否 | 是 | 是 | 是 Zoneinfo (高级时区) | 是 | 否 | 是 | 是 Gmon 性能分析 | 否 | 否 | 是 | 是 调试特性 | 否 | 否 | 否 | 是 各种 Linux 扩展 | 是 | 是 | 部分 | 是
目标架构对比 | musl | uClibc | dietlibc | glibc ---|---|---|---|--- i386 | 是 | 是 | 是 | 是 x86_64 | 是 | 是 | 是 | 是 x86_64 x32 ABI (ILP32) | 实验性 | 否 | 否 | 不符合标准 ARM | 是 | 是 | 是 | 是 Aarch64 (64-bit ARM) | 是 | 否 | 否 | 是 MIPS | 是 | 是 | 是 | 是 SuperH | 是 | 是 | 否 | 是 Microblaze | 是 | 部分 | 否 | 是 PowerPC (32- and 64-bit) | 是 | 是 | 是 | 是 Sparc | 否 | 是 | 是 | 是 Alpha | 否 | 是 | 是 | 是 S/390 (32-bit) | 否 | 否 | 是 | 是 S/390x (64-bit) | 是 | 否 | 是 | 是 OpenRISC 1000 (or1k) | 是 | 否 | 否 | 未上游 Motorola 680x0 (m68k) | 是 | 是 | 否 | 是 无 MMU 微控制器 | 是, elf/fdpic | 是, bflt | 否 | 否
构建环境对比 | musl | uClibc | dietlibc | glibc ---|---|---|---|--- 对遗留代码友好的头文件 | 部分 | 是 | 否 | 是 轻量级头文件 | 是 | 否 | 是 | 否 无需原生工具链即可使用 | 是 | 否 | 是 | 否 尊重 C 命名空间 | 是 | LFS64 问题 | 否 | LFS64 问题 尊重 POSIX 命名空间 | 是 | LFS64 问题 | 否 | LFS64 问题
安全/加固对比 | musl | uClibc | dietlibc | glibc ---|---|---|---|--- 关注边界情况 | 是 | 是 | 否 | 过多的 malloc 安全的 UTF-8 解码器 | 是 | 是 | 否 | 是 避免超线性大 O | 是 | 有时 | 否 | 是 栈溢出保护 | 是 | 是 | 否 | 是 堆损坏检测 | 是 | 否 | 否 | 是
其他对比 | musl | uClibc | dietlibc | glibc ---|---|---|---|--- 许可证 | MIT | LGPL 2.1 | GPL 2 | LGPL 2.1+ w/exceptions
注释
总体说明
在表的每个比较中,每个库都标记为红色、黄色或绿色。 红色或黄色表示该库未能支持某个功能或满足 某些用户 可能期望的最佳条件。
对于涉及测试和测量的比较,所比较的特定库版本是:
- musl 1.1.5
- uClibc 0.9.33.2 (Buildroot 2015.02)
- dietlibc 0.32
- glibc 2.19
请注意,之前版本的比较包括 eglibc 而不是 glibc,主要是因为基于 Debian 的发行版在使用 eglibc 分支,当时 glibc 本质上处于无人维护状态。 由于 eglibc 的大部分已合并回 glibc 并且 eglibc 正在停止维护,因此已根据 glibc 更新了比较。
臃肿程度对比
粗略地说,“臃肿”用于指不有助于应用程序功能的开销成本。
所有数字都是根据我在使用的系统上可用的这些库的版本测试得出的近似值。 我使用了 size(1)
而不是文件大小,因为静态库文件对于包含的对象文件来说,大约 80% 是 ELF 标头开销。 共享库比其静态等效库大的部分原因是它们包含用于长除法和其他数学函数的 libgcc 的一部分。
glibc 的大小总计包括 iconv 模块的大小,大约 5M,在“完整的 .so 文件集”图中。 这些对于提供某些功能至关重要,无论使用静态链接还是动态链接都应安装。
最小的 C 程序是:
int main() {}
我使用的“hello”程序是:
#include <stdio.h>
int main(int argc, char **argv) { printf("hello %d\n", argc); }
我这样编写是为了确保编译器无法将打印的字符串优化为常量,并将对 printf
的调用替换为对 puts
的调用。
开销以脏页衡量,即每个进程所需的交换支持的物理内存量。 这些是磁盘上程序映像的私有写时复制映射、堆、栈和匿名映射的混合。 使用 /proc/$pid/smaps
文件获取无限循环程序的数字。
动态链接开销很大程度上取决于动态链接器。 标准动态链接器的效率低下导致了动态开销的 12-16k。 理想情况下,替换它可以将静态链接和动态链接程序之间的开销差异降至单个页面。
应该注意的是,uClibc 经过测试时启用了许多可选功能,特别是 locale。 由于 uClibc 的 locale 支持中的一个错误(设计缺陷),即使在从未使用 setlocale
的程序中,locale 加载代码和 malloc
也会被链接。
资源耗尽时的行为
这些比较处理了当可用内存或其他系统资源极低时,各种接口的鲁棒性。 报告失败在理论上是最佳行为时会显示为绿色; 当备用实现可以成功执行操作而无需资源使用时,它会显示为黄色。
线程局部存储涵盖了以下两种情况:尝试创建新线程时,没有足够的可用内存来满足所有加载模块的线程局部存储要求; 以及尝试通过 dlopen
加载具有线程局部存储的新模块时,没有足够的可用内存来满足所有现有线程的存储要求。
在 pthread_cancel
的情况下,NPTL 在第一次取消请求时在运行时动态加载 libgcc_s.so.1
,如果因任何原因(包括但不限于资源耗尽)加载失败,则中止该程序。
性能对比
所有这些数字都是在我特定的基于 Intel Atom N280 的机器上,在 UTF-8 locale 中,使用我的 libc-bench
套件获得的。 它们并非旨在严格,只是为了粗略了解相对数量级性能。
微小和大块内存分配数字来自 b_malloc_tiny1
和 b_malloc_big1
。 内存分配竞争测试衡量当两个线程同时执行分配和释放操作时的 malloc 性能。 在第一个测试(本地)中,每个线程释放其自己的分配。 在第二个测试(共享)中,分配和释放线程通常不是同一个,打破了线程本地 arena/cache 优化。
strstr 数字是任何 strstr 测试所花费的最大时间,以衡量最坏情况时间; 哪种情况最坏因实现而异。 glibc 的糟糕性能可以通过删除禁用短于 32 字节的 needle 的最佳优化的代码来轻松修复; 通过此更改,它应该与 musl 匹配或略微优于 musl。
线程创建和加入数字来自 b_pthread_createjoin_serial1
。
ABI 和版本控制对比
向后兼容性是指通常的情况,即库的新版本与针对旧版本编译的程序兼容。“向前兼容性”是我可能发明的术语,但它旨在传达的想法是,只要程序不依赖于旧版本库中缺少的功能,旧版本的库就与针对新版本编译的程序兼容。 在后一种情况下,程序将简单地在(静态或动态)链接时因缺少符号而失败。
也许考虑“向前兼容性”的最简单方法是,除非程序实际需要您的版本中缺少的功能,否则您无需升级库。
符号版本控制和向前兼容性都有优点,但它们本质上是互斥的。
“原子升级”是指单个原子文件系统操作升级库,在动态链接程序可能无法运行期间没有竞争条件窗口。 确保原子升级的规范方法是将整个库放在单个 .so
文件中。
算法对比
在比较子字符串搜索算法时,m 通常指 needle(子字符串)的长度,而 n 通常指 haystack(要搜索的字符串)的长度。 twoway 算法是 O(n),并且通过 musl 使用的类似 Boyer-Moore 的改进(以及 glibc 使用的,但仅适用于非常长的 needle),典型运行时与 n/m 成正比。 naive 算法是 O(nm)。
回溯正则表达式实现很容易编写,但在许多类似现实世界的表达式中具有病态的糟糕性能,并且无法利用语言的规律性。
dietlibc 使用的 naive 快速排序在堆栈上具有 O(n) 空间要求,这意味着它在实际使用中会并且将会导致堆栈溢出崩溃。 这可以通过选择递归的最佳顺序并执行尾调用优化来修复。 快速排序的时间复杂度也是 O(n²),虽然典型性能要好得多,但最坏情况下的性能非常糟糕。 Shell 排序通常是 O(n α),其中 1<α <2,尽管可以将其优化为 O(n(log n)²). 确定 uClibc 版本的特征需要一些分析。 Smooth sort 是 O(n log n) 并且平滑地内插到 O(n),大致与输入已排序的程度成正比。 Intro sort 是快速排序的一种变体,它检测最坏情况下的递归并切换到堆排序以保持 O(n log n) 边界。
特性对比
精确的浮点数打印是指当指定的精度足够高时,能够使用 printf
打印浮点数的精确值。 例如,作为双精度值,0.1
是 0.1000000000000000055511151231257827021181583404541015625,它是 diadic 有理数 115292150460684704/260。 也许更有用的是,(完全可表示的)数字 2-60 应打印为 0.000000000000000000867361737988403547205962240695953369140625 而不是一些不精确的近似值。
完整的 C99 数学库包括所有先前存在的数学函数的新单精度和扩展精度版本,以及它们的复杂版本和 tgmath.h
。
POSIX 线程是指具有真实 POSIX 语义的线程,而不是历史上损坏的 LinuxThreads(其中每个线程的行为都像一个不同的进程)或类似的实现。
POSIX localedef 是指定义自定义 locale(包括字符集等)的能力。
TCB 密码 是来自 Openwall 的一项功能,它将密码哈希从 /etc/shadow
移动到 /etc/tcb/_username_ /shadow
。 这允许用户更改密码,并允许以用户身份运行的程序(例如,屏幕锁定器)验证用户的密码,而无需特殊的 suid 或 sgid 权限。
Linux 扩展是指 Linux 提供的超出 POSIX 和历史行为范围的内核接口 - epoll
、signalfd
、扩展属性、功能、模块加载等等。
目标架构对比
glibc 的 x32 支持中存在许多一致性问题,最值得注意的是它将 struct timespec
的 tv_nsec
成员定义为 long long
,尽管 POSIX 和 C11 都要求它的类型为 long
。 这种差异影响了与格式化打印函数的使用以及指向该成员的指针的使用等等。 许多其他接口也已更改为在结构中使用 long long
而不是 long
; 在许多情况下,没有标准管理受影响的接口,但这些更改破坏了在其他文档(如 Linux 手册页)中发布的接口约定。
uClibc 的 microblaze 端口标记为部分,因为它缺乏对线程和可能其他核心功能的支持。
标记为“实验性”的端口是那些记录为这样的端口; 这可能意味着某些功能已损坏和/或 ABI 不稳定。
构建环境对比
“对遗留代码友好的头文件”意味着系统 C 头文件是从历史实践发展而来的,并且默认情况下定义/声明了许多不应该定义/声明的内容,但某些遗留代码可能需要这些内容。 它们通常依赖于深层嵌套的包含和复杂的条件编译。
“轻量级头文件”大致相反,从头开始编写以匹配 C 和 POSIX 标准,具有最少的嵌套包含和预处理器条件。 这带来了编译大量小文件的巨大性能优势,但这也意味着依赖于某些特定于实现的遗留特性的编写不佳的程序可能需要进行细微的修复才能编译。
如果没有专门针对它们构建 GNU binutils 和 gcc(即原生工具链),几乎不可能使用某些审查的库。 其他库可以轻松使用最初针对不同库的现有工具链,覆盖某些编译器和链接器选项以使用备用库实现。
尊重 C 和 POSIX 命名空间意味着标准 C 和标准 POSIX 函数和头文件使用的命名空间符合这些标准关于哪些名称保留给实现与保留给应用程序的说法。 一个常见的非一致性领域是将 open
、lseek
等函数重新映射到 open64
、lseek64
等 - 这些名称保留给应用程序。 这在表中标记为“LFS64 问题”。
安全/加固对比
“关注边界情况”意味着该库遵循一种通用理念,即小心地支持所有可能的输入,这些输入不会明确调用 未定义的行为,尤其是当输入可能来自程序外部的源时。 当某些不应有任何故障情况的接口由于内存耗尽的可能性而创建了人为的故障时,会在比较中标记过度使用 malloc
。
不安全的 UTF-8 解码器是一种无法检测无效序列并恰好将它们解码为有效字符别名的解码器。
堆损坏检测意味着 malloc
会尽力检测、报告并在检测到双重释放、尝试释放不是通过 malloc
获得的指针等时中止。
其他对比
许可证的选择会影响标准库实现的可用性。 GPL v2-only 被标记为“最差”的选择,因为它与大量开源/自由软件不兼容,即任何使用 GPL v3-only 的软件。 LGPL v2.1-only 的问题要少得多; 它不允许通过与 LGPL v3-only 代码合并来创建新的 LGPL 许可的库,但它允许合并的程序在 GPL 的版本 3 或更高版本下发布。 LGPL v2.1-or-later 非常灵活,MIT 或 BSD 更是如此。