欢迎光临散文网 会员登陆 & 注册

记一次程序优化

2023-08-23 16:31 作者:SAS骆豪  | 我要投稿

背景

大约在一年前,我接触到了 IN 操作符的特殊用法:

var in array_name

该用法将变量的值直接与数组中每个元素的值进行比较,而无需借助循环语句,返回值则是布尔类型,形式简洁且优美。为了记住此法,我尽可能地在自己的工作中使用它,其中一个用例是:如何找到字符串中首个汉字的位置?测试数据集如下所示:

输入数据集截图

1行纯粹是单字节字符,第2行至第5行在不同位置开始出现中文,第68行则是外文文字(阿、日、俄)。我当时给出的解决方案如下:

输出数据集截图

我利用 unicode() 函数,将基本汉字集(U+4E00-9FA5)存入临时数组,然后遍历源字符串中的所有字符,判断其是否为数组中的一员,在这里我用到了 IN 操作符的特殊用法,当判断返回真值时,查找当前字符在源字符串中的位置,并将其作为结果输出。


自定义函数

最近,我又翻到了这个用例,我认为它与 SAS 的内建函数 anynum()anyalpha()anyspace()等非常类似,都是用来查找源字符串中特定类型字符的首次出现位置。于是,我诞生了制作自定义函数 anyhan() 的想法,将之前的代码片段进行封装,并做了必要的调整,第一版设计就诞生了:

我使用 PROC FCMP 进行自定义函数的设计,主要有三处调整:

  1. FCMP 过程步对数组的定义与数据步不同,数组下标起始值和_temporary_ 关键字发生了改变;

  2. FCMP 过程步不支持 var in array_name 用法,调整成了do 循环进行遍历查找;

  3. 由于增加了一层循环,循环的跳出和结果变量的赋值稍有改变;

这个函数定义完备,并可以使用,但是速度却不尽如人意。我尝试将它用在测试数据集上,每条观测调用该函数100次,居然花费了7秒多的时间。测试程序如下:

我稍加思索,认为不是每个字符都需要判断是否为汉字,比如单字节字符明显就可以跳过,于是我将函数定义第14行的:

do j=1 to dim(_han_);

改为:

if length(kchar)>1 then do j=1 to dim(_han_);

速度有了一点点改善,同样的测试从7秒变成了约6秒,但仍然不能使我满意。我盯着程序,要找出是谁拉了后腿,注意到在原始的解决方案中,数组赋值的操作有限定条件:if _n_=1,而 anyhan() 中是没有的,会不会是它耗时太多呢?我立刻为自定义函数的数组赋值也加上这个限定条件,但接下来又陷入泥潭:耗时减少到1.5秒,但结果变量的值全为0!看来,_n_ 并没有被 FCMP 当成数据步中的自动变量。

我无法再阅读出蛛丝马迹,但我很快想到还有另外一种手段可用,那就是删代码调试法。这种方法脱胎于控制变量法思想,在保证代码可以运行的前提下,每次删除一部分代码,从而观察到被删除的代码对结果的影响。我早就怀疑自定义函数中数组的赋值行为与数据步不同,因此直接就修改了函数定义:

运行同样的测试程序,耗时约4.8秒,拖慢速度的源头找到了!找到它不容易,要优化它就更麻烦些。我深入地思考:制作汉字集数组是为了后面的比较提供方便,但每条观测都执行20901次循环,调用20901 unicode() 函数,有这个必要吗?显然,要执行查找的源字符串一般不会用到这么多个不同的汉字,《现代汉语常用字表》不过3500字,如果限定在临床研究或金融研究领域就更少了。同样是判定指定字符是否处在汉字基本字集内,我不一定要将该字符与基本字集元素逐个比较,基于基本字集编码连续的特点,我也可以将该字符的编码与基本字集的编码范围进行比较,这样就无需定义汉字集数组,一定能快上许多。我改了改,又调试了一会,第二版设计也出来了:

在逐个截取源字符串中的字符时,使用 unicodec() 获取当前字符的 unicode 编码值,编码值以 \u 开头,后面跟着2位、4位或8位十六进制数字,接着配合使用 substr() input() 将字符型编码值转换为数值,最后再与基本字集的编码上下限进行比较,当比较结果为真时,查找当前字符在源字符串中的位置,并将其作为结果输出。

我再次将它放在同样的测试程序上,大概一眨眼的功夫,程序就成功地给出了结果,这十分令我振奋!我再次回顾函数定义,发现还有两点可以改进的地方:

  1. 当前只剩下一层循环,goto 语句的使用或许存在争议,可改用其它跳出循环做法;

  2. 既然是对字符编码进行比较,就不一定要把编码值转换为数值,字符型的编码值也可以比较;

基于这两点改进思路,我很快给出了第三版的设计:

在新的设计当中,do until() 代替了原先的 goto 语句+标签语句的组合,程序的可读性更好。另外,编码值的比较被设计成基于字符串的比较方式,即从左至右逐个比较字符的的大小,由于编码值的构成特点,这实际上就是在挨个比较编码值中组成十六进制值的字符的大小,更具体一些,就是在比较这些字符的 ASCII 码值的大小,例如4(码值52)小于9(码值57),而F(码值70)大于E(码值69)

我十分满意这个设计,它足够简洁,又易于扩展,假如将不等式两端的上下限换为 \u3040 \u30FF,就可以用来匹配日文字符,换为 \u0400 \u052F 就可以匹配俄文字符。

我使用 SAS 9.4 M8 进行开发和测试, CPU Intel i7-1265U,不同设计的 anyhan() 函数运行时间如下(运行10次取平均):

不同设计的 anyhan() 函数运行时间




记一次程序优化的评论 (共 条)

分享到微博请遵守国家法律