问题:输入一个长度为 N 的字符串 S(其中每个字符都是可打印的 ASCII 码),将其中的大写字母转换为小写字母。
方法1:直接能考虑的转换方法是依次比较 S 中的每个字符,然后对其进行转换。
for(int i = 0; i < N; i++) {
if ((s[i] >= 'A') && (s[i] <= 'Z') ) {
s[i] += 'a' - 'A';
}
}
这样的操作方法存在以下问题: (1)每次仅仅对一个字节进行操作,不能发挥 32 位处理器全字操作的能力; (2)每个字符转换需要进行两次比较,并可能发生条件分支,可能导致处理器的流水线中断。
分析大小写字母的 ASCII 码规律,你会发现:同一个字母的大小写 ASCII 码差异仅仅在第 5 位。 例如, ’A’ 的 ASCII码表示为 0100_0001, ’a’ 的 ASCII 码表示为 0110_0001, 因此从大写字母到小写字母的转换仅仅只需要进行对大写字母的第 5 位或上 1,即 ‘A’ | 0x80 ==> ‘a’.
方法2:
int i;
/*将 char类型指针转换成 unsigned int类型的指针,
这样 p[i]就是一个4字节的整数了,即每次可以处理4字节了
*/
unsigned int *p = (unsigned int *)(s);
// 共有 N>>2 个完整的字
for (i = 0; i < (N>>2); i++) {
unsigned int tA = 0x41414141; // 0x41为'A'的ASCII码,故这里为 AAAA
unsigned int tZ = 0x5A5A5A5A;; // 0x5A为'Z'的ASCII码,故这里为 ZZZZ
unsigned int d0 = p[i];
// 将每个字节的最高位置为 1(注:ASCII最高位只能为 0)
unsigned int d = d0 | 0x80808080; // 0X80 ⇒ 1000_0000
/*
字 d 有4个字节,分别表示 4个字符(只算每个字节的低7位),将它们分别与 'A'做差,
如果字符 ci >= 'A',则对应字节的最高位还是 1;反之,则为 0(次高位向最高位借位)。
*/
unsigned int cA = d - tA;
// 保留每个字节的最高位,其余位清零
cA &= 0x80808080;
/*
首先,将tZ的每个字节的最高位设为1,
然后,将它们分别与 d0 的4个字符做差,
如果字符 'Z' >= ci,则tZ对应字节的最高位还是 1;反之,则为 0(次高位向最高位借位)。
*/
unsigned int cZ = (tZ | 0x80808080) - d0;
// 保留每个字节的最高位,其余位清零
cZ &= 0x80808080;
// cA和cA每个字节的最高位同时为1时, 'A'<= ci <='Z'
unsigned int c = cZ & cA;
// 将 '1' 移至第5位(从右往左数,从0开始)
c >>= 2;
d = d0 | c;
p[i] = d;
}
i--;
// 处理剩余字符 (小于4个字符)
for (i<<=2; i < N; i++) {
unsigned char tA = 0x41;
unsigned char tZ = 0x5A;
unsigned char d0 = s[i];
unsigned char d = d0 | 0x80;
unsigned char cA = d - tA; cA &= 0x80;
unsigned int cZ = (tZ | 0x80808080) - d0; cZ &= 0x80;
unsigned char c = cZ & cA;
c >>= 2;
d = d0 | c;
s[i] = d;
}
上述代码中首先将 d0 中四个字节的每个字节的最高位设置位 1, 然后使用两次 32 位减法操作, 检查每个字节是否处于 ’A’ 和 ’Z’ 之间。其关键点是设置每个字节的最高位为 1,其主要目的是隔离每个字节减法之间的借位链。
N = 500000 时,测试 10000次,方法2和方法1所需时间如下:
扩展:小写字母转为大写字母时,也可利用上述思路,只是此时需要将字母的第5位与0,如:‘a’ & 0xDF ==> ‘A’。