Swift中的字符串,字符和性能-深入探讨
扫描令牌所用时间的三分之二全部花在了用于Character
类型的初始化程序中。 到底是怎么回事?
初始化程序的第一个参数标签提供了一个小提示: _builtinExtendedGraphemeClusterLiteral
。 注意单词“ literal”。此初始化程序不用于从我要遍历的字符串中提取Character
值; 它用于从源代码中其他地方的文字文本创建Character
值。 在nextToken
下找到这些标记的唯一地方是我的令牌生成器的switch/case
模式。 他们真的造成那么多的开销吗?
我们可以让Swift编译器使用-emit-sil
选项发出“规范SIL”(Swift中级语言),以更仔细地了解如何将这些case
模式编译为较低级的代码。 (我在存储库中包含了一个小脚本,该脚本可以执行此操作,并且还可以分解Swift符号。)让我们找到与匹配逗号字符的case
模式对应的SIL(为简洁起见,重新格式化了行号和范围):
%448 = string_literal utf8“,”
%449 =应用%26(%448,%23,%24,%25):
$ @ convention(method) (Builtin.RawPointer,Builtin.Word,
Builtin.Int1,@ thin Character.Type)-> @owned字符
%449
的第二行和第三行所示的方法签名%449
其放弃,但我们可以通过查看值%26
来确认它,该值是被调用的函数:
%26 = function_ref @ Swift.Character.init(
_builtinExtendedGraphemeClusterLiteral:Builtin.RawPointer,
utf8CodeUnitCount:Builtin.Word,
isASCII:Builtin.Int1)-> Swift.Character :
$ @ convention(method)(Builtin.RawPointer,Builtin.Word,
Builtin.Int1,@ thin Character.Type)-> @owned字符
这是什么意思呢?
这意味着, 每次通过扫描循环(即,针对字符串中的每个字符)时,Swift都会调用此初始化程序来创建case
模式中的每个字符,以便将当前字符与该模式进行比较,直到找到匹配项为止;并且该初始化程序如果您习惯于字符类型实际上只是一个数字代码单元的语言,则它的成本将大大超出您的预期。
通过查看Swift标准库源代码,我们可以看到将字符串文字(例如","
)转换为Character
时发生的动作序列:
- 编译器将UTF-8编码的文字表示形式嵌入可执行文件的数据段中。
- 在使用文字的地方,Swift调用
Character.init(_builtinExtendedGraphemeClusterLiteral:utf8CodeUnitCount:isASCII)
,Character.init(_builtinExtendedGraphemeClusterLiteral:utf8CodeUnitCount:isASCII)
其传递步骤1中嵌入的字符串数据的地址(源)。 - 反过来,此初始化程序在
String
上调用相同的初始化程序。 最终,分配了StringBuffer
(源)。 - 最后,将
String
转换回一个Character
值(源)。 根据其UTF-8表示形式的大小(请记住,一个“字符”可以包含多个代码单元),它将紧凑地存储为不大于63位的整数,或者将保留整个字符串缓冲区(源)。
换句话说,只问“ does ch == ","
?”这个问题ch == ","
我们最终就在内存中分配了一个甚至不需要的字符串,因为","
很容易放入8位,更不用说63。
为什么这么复杂?
这是我们为Unicode正确性和拥有与用户看到的内容而不是字符串的内部表示形式相对应的字符模型而付出的代价。 能够接受“città”之类的字符串并且知道它是五个字符长,无论它是五个还是六个代码点长(“ citt” +“à” vs.“ citt”),既强大又方便。 +“ a” +结合重音符号)。 同样,希望能够比较那些字符串并确定它们在规范上是等效的,即使它们的内部表示形式不同。
编译器可以在这里做得更好吗?
可能吧 在将字符串文字转换为Character
,我想像一下,如果再多花一点功夫,编译器就可以确定该值适合紧凑表示形式,并直接发出该代码,而忽略了上述字符串到字符的转换以上。
您不必忍受这样的执行时间。
让我们谈谈不依赖于编译器更改而可以改善运行时的方法。
首先,如果花费了令牌生成器的大部分时间来初始化要匹配的Character
值,那么如果我们自己对它们进行预初始化会怎样?
仅进行此更改之后,新的执行时间大约是以前的三分之一:
CharacterBasedTokenizer:
..... 686.6116408 ms±14.3905875334629 ms(平均值±SD)
再次查看时间配置文件,我们可以看到现在大部分时间都用在CharacterView
的迭代器中: