以小写转换大写为例子
字节操作
这个小写转换,很容易就知道,给每一位都减去'a'-'A'
的值,也就是32即可。换成字节的话,直接减去0x20202020
即可。
话说当初设计ASCII表的时候,故意把大小写字母的间隔调整为32的吗,这样子这个差值就只有一个bit是1了,做很多运算都非常方便,比如接下来的。
难就难在,我们不能把非小写的字母改掉,这就带来了问题,我们需要判断:每一个字节都在[97,122]这个区间内。
判断字节是否大于等于97
我们不可能一个一个字节的判断,这样就回到了一个一个字节的处理方法上了,我们要用32比特的整数。
那么跟刚才减去0x20202020
这个做法类似,我减去每个字节都是97的数,也就是0x61616161
。但是,我们不能直接减0x61616161
。减法的实现就是加上负的减数,而我们知道,负数在计算机里面是以补码形式存储的。补码是取反然后加1,我把0x61616161
取反加一之后,得到的数是0x9E9E9E9F
,就不是本来那个每个字节字节减去97的效果了。我们应该用-97的补码表示,取得这个加的数,也就是0x9F9F9F9F
,这样就是每一个位都减了97。
比如,0x61626364
的输入,得到的是0x01020303
,高位都是0,说明这些数,都大于等于97。
但是,还有一个问题,不知道大家发现了没有。
两个二进制数相减,变成一个数加上一个负的另一个数,如果得到一个正的结果,将会有一个进位1产生。这在字节操作是没有这个问题的,进位1因为超出了字节,被忽略了。
但是,在这里就有问题了。
这个进位1,加到了下一个字节里面去,把本来加的结果给继续加了1。如果我输入,0x60616161
的话,我得到的,就是0x00010100
,最高的字节居然是0了!0x60
是小于97的,产生了错误的结果。
因此,我们必须减去进位。
进位的获取也很简单,只需要加上0x9F9F9F9F
之后,一样与上0x80808080
取字节最高位,然后再取反,因为最高位为0的是有进位的,我们取反让进位1出现了。再向左移1位,对齐下一个字节的数。然后,用原本加完了0x9F9F9F9F
的数,减去这个得到的进位,就是本来应该是的结果了。
比如,还是0x60616161
这个例子,按照下列步骤处理。
- 加上
0x9F9F9F9F
得到0x00010100
- 取得最高位,与上
0x80808080
,得到0
- 取反,异或
0x80808080
,得到0x80808080
- 左移1位,得到
0x01010100
- 用
0x00010100
减去0x01010100
,就得到应该得到的结果,0xFF000000
0xFF000000
与上0x80808080
,得到结果,0x80000000
0x80000000
右移2位,得到0x20000000
0x20000000
异或0x20202020
,得到最终结果0x00202020
经过这几个步骤之后,我们去掉了第一个字节的32,第一个字节就不需要减去32了。
判断字节是否小于等于122
做法与上面是类似的,只需要每一位加上-123,也就是0x85
减去处理完的0x20
在处理完0x20202020
得到最终的减数之后,我们终于可以使用本来的数字,减去这个的出来的减数,以变成大写了。
时间性能对比
用了下面两个用例函数来测试了下性能
func BenchmarkToUpperCaseUnsafe(b *testing.B) {
str := "@ABCDEFGHIJKLMNOPQRSTUVWXYZ_`abcdefghijklmnopqrstuvwxyz{|}"
for i := 0; i < 1000; i++ {
str += "@ABCDEFGHIJKLMNOPQRSTUVWXYZ_`abcdefghijklmnopqrstuvwxyz{|}"
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
toUpperCaseUnsafe(str)
}
}
func BenchmarkToUpperCaseByte(b *testing.B) {
str := "@ABCDEFGHIJKLMNOPQRSTUVWXYZ_`abcdefghijklmnopqrstuvwxyz{|}"
for i := 0; i < 1000; i++ {
str += "@ABCDEFGHIJKLMNOPQRSTUVWXYZ_`abcdefghijklmnopqrstuvwxyz{|}"
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
toUpperCaseByte(str)
}
}
结果是按32位处理的每次59550ns,字节处理每次52542ns,还要慢。其中为了提高性能使用了unsafe包将byte数组强制转换成了int数组,也只能是这样的结果。
应该还可以优化,或者如果是64位处理呢?
极致优化方法
之前还是太蠢没有想到一个不用判断进位的方法。
本来ascii就设计成7位的二进制数,那么我完全可以在第八位设置1。如果低7位的相减结果是正数,那么不需要借第8位的1来减。这样,上文所说的进位,就不会传递给下一个字节了。
另外,0x20202020
这个数完全可以不需要使用,只需要拿两个小于97和小于等于122的结果做一次位运算就行。
具体是,首先我们构造一个数,在97的第8个二进制位设置成1,也就是0xE0
。我们新的方法,用0xE0E0E0E0
减去输入值。如果对应字节上低7位的数小于97的话,那么减出来是个正数,前面第8位的1保持不动。如果是个负数,则需要向第8位借位,此时结果第一位就是0。
小于等于122的判断类似。
然后,我们得到两个结果,一个是判断小于97的数l97
,一个是判断小于等于122的数le122
,我们需要取中间的数,那么给l97
取反(其他7位不变),再与上le122
,高8位为1的,就是在区间[97,122]之内的数了。
然后右移两位,与输入结果进行一个异或运算,这相当于减去了32。
下面是go语言实现
// 极致性能
func uint32toUpperCaseV3(char uint32) uint32 {
// 0xE1是96前面一位为1
num1 := uint32(0xE0E0E0E0)
// 0xFA是122前面一位为1
num2 := uint32(0xFAFAFAFA)
// 字节最高位为1的就是小于97的
lessThan97 := (num1 - char) & 0x80808080
// 字节高位为1的就是小于122的
lessThanEqualTo122 := (num2 - char) & 0x80808080
// smallerThan97最高位为0的和smallerThan123最高位为1的,就是在区间内的,与其来
between97And122 := (lessThan97 ^ 0x80808080) & lessThanEqualTo122
return char ^ (between97And122 >> 2)
}