
同一条 sort 命令,换个 locale 就跑出完全不同的结果,性能上也差一大截。这类差别在日志分析和去重脚本里都会碰上,而它来自 POSIX 对 locale 的定义,不是某个实现的怪癖。
脚本 tools/verify-locale-c.sh,macOS 26.2,sort 2.3-Apple (197),shell 是 bash 3.2。
POSIX 把「排序」交给 locale 决定
POSIX 对 LC_COLLATE 的说明是:它影响排序的次序,也就是哪些字符被当作相等的、以及默认的排序顺序。LC_CTYPE 管的是字符分类与大小写转换,正则里的字符类归属也在这里定。LC_ALL 一旦设成 C,这两项都退回到最朴素的定义:按字节值比较,字符类只认 ASCII。
也就是说 LC_ALL=C 不是「关掉本地化」这么轻描淡写,它是换了一套比较规则:[a-z] 只表示 26 个小写字母,é 和 É 是不同的字节,次序由字符编码决定。

同一批行,两种次序
=== 1. sort:同一批行,两种 locale ===
输入 : b a B A _und 1num
en_US.UTF-8 : _und 1num a A b B
LC_ALL=C : 1num A B _und a b
字节位置 : 1=0x31 A=0x41 _=0x5f a=0x61
C 那一行就是字节序:数字在前,大写在前,下划线夹在大写与小写之间。en_US.UTF-8 那一行把下划线排到了最前,大小写也改成混着排,因为它的比较用的是另一套权重。同一批文件名,两种 locale 能给出两个顺序。
汉字在 en_US.UTF-8 下比不出先后
输入 : 阿七 张三 李四 王五 陈六
en_US.UTF-8 : 阿七 张三 李四 王五 陈六
LC_ALL=C : 张三 李四 王五 阿七 陈六
LC_ALL=zh_CN.UTF-8: 阿七 陈六 李四 王五 张三
换个输入顺序再排一遍(判断是不是「汉字之间比不出大小」)
输入 : 王五 阿七 陈六 张三 李四
en_US.UTF-8 : 王五 阿七 陈六 张三 李四
LC_ALL=C : 张三 李四 王五 阿七 陈六
LC_ALL=zh_CN.UTF-8: 阿七 陈六 李四 王五 张三
汉字和 ASCII 混排时汉字排在哪
输入 : b 阿 Z 1 _
en_US.UTF-8 : 阿 _ 1 b Z
LC_ALL=C : 1 Z _ b 阿
第二组是关键证据:把输入顺序换一遍,en_US.UTF-8 的输出跟着输入走,说明这几个汉字之间比较结果相等,sort 的稳定排序于是保留了原序。zh_CN.UTF-8 给出的是拼音序,C 给出的是字节序。
C 有一处容易被忽略:UTF-8 编码下汉字的字节值普遍大于 ASCII,所以升序排列时汉字全部堆在末尾。混排那一行把这一点显示得很清楚,C 把汉字排在最后,en_US.UTF-8 把它排在最前。
非 ASCII 的大小写折叠只在 UTF-8 下发生
=== 3. grep -i 对非 ASCII 的大小写折叠 ===
输入: café CAFÉ naïve NAÏVE
en_US.UTF-8: grep -i 'café' → café CAFÉ
LC_ALL=C : grep -i 'café' → café
en_US.UTF-8: grep -i 'NAÏVE' → naïve NAÏVE
LC_ALL=C : grep -i 'NAÏVE' → NAÏVE
en_US.UTF-8: grep 'café' 大小写敏感 → café
要找带重音的名字,LC_ALL=C 会让 grep -i café 漏掉 CAFÉ,得把两种写法都写出来。同一份脚本在本地手测时是全命中,进了 CI 容器(LC_ALL=C 是常见默认)就开始漏,问题就出在这一行。
同一个脚本里还有两处相反的结论,一并记下来:[a-z] 区间表达式在两种 locale 下没有差别,tr 'a-z' 'A-Z' 也没差别,é 两种情况下都不会被 a-z 抓到。这两个是我原本以为会踩坑、实测下来没踩的地方。区间表达式之所以看着稳,是因为样本里只有单字节的 a 和 z 被命中,真要拿它匹配带重音的全名,两种 locale 都一样漏,得改用显式的字符集合。
一百万个随机串,差七倍
=== 6. 耗时:100 万行排序,两种 locale(各跑两遍取第二遍)===
LC_ALL=en_US.UTF-8 第 1 遍: 1.44s
LC_ALL=en_US.UTF-8 第 2 遍: 1.47s
LC_ALL=C 第 1 遍: 0.19s
LC_ALL=C 第 2 遍: 0.19s
文件行数: 1000000
差在比较函数上:UTF-8 collation 每次比较要走多字节解码与权重表,C 直接比字节。在只需要去重或者按字节序排的场景,LC_ALL=C sort 是免费的加速;而一旦数据里有中文名字、需要人读的顺序,这个加速会换来一个错误的顺序。
怎么选
判断标准只有一条:这个排序结果给谁看。给机器看的用 LC_ALL=C,像是去重、生成中间文件,快且稳定,各机器结果一致。给人看的就必须显式指定能给出该语言顺序的 locale,并且把这一项写进部署配置,别指望环境默认值。
脚本开头那句 LC_ALL=C 想写成全局变量的话,建议只包在需要的命令上,用 env LC_ALL=C sort 这种前缀写法,不要 export。一旦导出,同一段脚本里后面所有 grep -i 都会跟着变,而这类连带影响通常要等到线上漏数据才会被发现。