如果我有一个固定大小的填充ascii数字的char数组,我知道它指的是一个无符号整数,那么将其转换为无符号整数的最快方法

If i have a fixed size char array of padded ascii digits which I know refers to an unsigned integer, what is the fastest way to convert it to such?

本文关键字:无符号整数 一个 方法 转换 有一个 填充 ascii 如果 我知道 数组 数字      更新时间:2023-10-16

假设我有一个char buf[12];,我知道它总是在左边用空格填充一个右对齐的无符号数字。例如:_________329(其中_代表空间)。我能想到的解析它的最快方法是这样的:

while (*buf == ' ') buf++;
atoi(buf);

但我想知道是否有更快的方法,特别是atoi,因为我们知道它是无符号的,而atoi并没有假设它。。

我假设第一个字符是为"潜在符号"保留的,并且总是"空格"?因为否则,您只需要char[11],而不需要char[12]。无论如何,固定尺寸允许手动展开环路:

unsigned parse(const char(&b)[12])
{
return ((((((((((b[1] & 15))
* 10 + (b[2] & 15))
* 10 + (b[3] & 15))
* 10 + (b[4] & 15))
* 10 + (b[5] & 15))
* 10 + (b[6] & 15))
* 10 + (b[7] & 15))
* 10 + (b[8] & 15))
* 10 + (b[9] & 15))
* 10 + (b[10]& 15);
}

注意,& 15技巧对空格和零一视同仁,并将同时使用ASCII(空格=32,零=48)和EBCDIC(空格=48,零=240)。我还没有检查出其他字符编码:)

这实际上会比atoi快还是慢?找到答案的唯一方法就是测量。但在任何情况下,我都可能使用atoi,因为使用标准函数总是可以提高可读性。

首先,问问自己为什么要这么做。如果"buf"是一个很长的文件,你可能会受到Schmiel the Painter算法的影响,如果你有很多十进制数字,你可能在使用例如GMP乘以一个大数字时遇到问题

其次,考虑一下您所知道的标准库所不知道的内容。Dmitry建议使用"快速平台优化的strrchr",但strrchr无法解决在字符串中迭代的问题,而且strrchr实际上有额外的约束,比如搜索终止的null字符。

你可能知道一些事情,比如:

  • 你的数字永远不会是负数;也就是说,atoi不需要选择一个领先的+/-符号。你已经正确地注意到了这一点,然而,这可能不是时间的主要因素
  • 你的数字大多是短的还是长的,这决定了你应该开始在字符串的开头还是结尾寻找空格。值得注意的是,strrchr不知道字符串的长度,因此总是从一开始就读取,我正在研究的atoi的实现也是如此(在Newlib中)。您的示例代码还暗示了从字符串的开头进行搜索
  • 你的数字总是以10为基数。这消除了一些数学问题
  • 你的数字总是适合无符号长。是的,这是有保证的,因为它们是12个字符,但atoi不知道这一点,会尝试处理错误。此外,atoi()返回一个有符号整数,因此,如果您需要像1000000000这样的13位数字,则需要对其进行处理
  • 其他我没有想到的事情;但你可以

您应该从读取源开始。这个简单的练习可以收获很多!我最近一直在使用Newlib,并且已经下载并打开了它,所以这就是我要参考的,但GNU的glibc和Windows使用的任何东西可能都会有所不同。

乍一看,我看到了一个简单的优化:atoi只是对strtol的调用,或"string to long"(在我的平台上,int和long都是32位,"long-long"是变大所必需的)。编译器可能会将其优化为直接调用,但这可能会为我们节省一个周期。对于表面上对速度敏感的应用程序,只需立即调用strtol()。或者更确切地说,调用strtoul,"string to unsigned long",因为这就是您要做的。现在我们有了一个不调用任何其他值得注意的函数,让我们来看看它。暂时忽略可重入性的东西。小心括号,有些if有括号,而相关的else没有括号(这是糟糕的风格IMO,我喜欢到处都有括号)。

unsigned long _strtoul_r
(struct _reent *rptr, _CONST char *nptr, char **endptr, int base)
{
register const unsigned char *s = (const unsigned char *)nptr;
register unsigned long acc;
register int c;
register unsigned long cutoff;
register int neg = 0, any, cutlim;
/*
* See strtol for comments as to the logic used.
*/
do {
c = *s++;
} while (isspace(c));
if (c == '-') {
neg = 1;
c = *s++;
} else if (c == '+')
c = *s++;
if ((base == 0 || base == 16) &&
c == '0' && (*s == 'x' || *s == 'X')) {
c = s[1];
s += 2;
base = 16;
}
if (base == 0)
base = c == '0' ? 8 : 10;
cutoff = (unsigned long)ULONG_MAX / (unsigned long)base;
cutlim = (unsigned long)ULONG_MAX % (unsigned long)base;
for (acc = 0, any = 0;; c = *s++) {
if (isdigit(c))
c -= '0';
else if (isalpha(c))
c -= isupper(c) ? 'A' - 10 : 'a' - 10;
else
break;
if (c >= base)
break;
if (any < 0 || acc > cutoff || (acc == cutoff && c > cutlim))
any = -1;
else {
any = 1;
acc *= base;
acc += c;
}
}
if (any < 0) {
acc = ULONG_MAX;
rptr->_errno = ERANGE;
} else if (neg)
acc = -acc;
if (endptr != 0)
*endptr = (char *) (any ? (char *)s - 1 : nptr);
return (acc);
}

从函数定义开始,我们注意到,如果我们的应用程序是单线程的,那么可以去除一些可重入性漏洞。还有一个char**ptr参数,它存储一个指针,指向经过解析数字的字符串,我们不需要它。也没有长度定义,所以它必须搜索null字符才能找到字符串的长度。

在这个应用程序中,*s被定义为一个寄存器,这在我的平台上是有意义的,但在你的平台上可能没有意义。还有一些其他定义的整数,我们不需要。

在do/while循环中,有一个对isspace()的调用,它检查空格、水平制表符、换行符、垂直制表符、提要和回车符。你只需要空间。此外,它从字符串的前面开始,然后返回。如果你的数字很少,那就改变它。

然后,我们做一些基础测试。碱基可以是0,允许自动检测碱基(需要周期),如果是8或16,则允许前导"0"或前导"0x",我们不需要知道或测试。

接下来,我们创建"cutoff"answers"cutlim"变量;你不需要这些,因为表面上你不需要范围检查。

最后,我们得出循环的结论。有一个if\else-if\else块,用于确定具有isdigitisalphaisupper函数的字符类型和数值。这些代码包含了一些依赖于facy语言环境的代码;我们似乎可以假设十进制值,它用单个CCD_ 23语句替换整个if/else-if/else块。

接下来,在if (c >= base)中还有更多的错误检查,这些错误检查是可以保留的。回想一下,C是无符号的,所以如果*s是一个空格(0x20)(小于"0",0x30),则其计算结果为(无符号)(0x30-0x20)=255-10,大于基数(10)。它并不完美,但它很好,也很便宜。

接下来,在if (any...块中进行一些边界检查,然后我们得到函数的实际内容:acc *= base; acc += c;。我们几乎无法对此进行优化,但如果我们有一个二进制基数,我们可以将其转换为移位。希望你的处理器上有一个快速的硬件乘法器,如果这是一个Arduino ISR,你就有麻烦了。如果你有乘法累加之类的DSP汇编指令,你可能想看看它们来加快速度。

在for循环之后,还有一些我们也可以忽略的错误处理和负数处理。

总之,如果你经常这样做,我会写一个新函数来处理你的特殊情况:

unsigned long TwelveCharDecimalStringWithLeadingSpacestoul(char *nptr)
{
register const unsigned char *s = (const unsigned char *)nptr;
register unsigned long acc;
register int c, base = 10;
do {
c = *s++;
} while (c == ' ');
for (acc = 0;; c = *s++) {
c -= '0';
if (c >= base) {
_errno = ERANGE;
acc = -1;
break;
}
acc *= base;
acc += c;
}
return (acc);
}

它去掉了CCD_ 27的一般性,并使用了您所做的稍微快一点的假设。然而,除非这种操作发生得太多,或者必须非常快,否则你可能会更好地使用更简单、更清晰、更安全、更灵活、总体上更好的

unsigned long result = 0;
char *begin = strrchr(buf, ' ');
result = strtoul(buf, NULL, 10);
if (result == 0 && errno == ERANGE)
// Handle error

EDIT:我写完了,我注意到FredOverflow发布了一个更好的答案。展开循环(我没有这么做,看起来没有必要,但如果必要,任何已知持续时间的循环都可以展开),并用& 15做了一个巧妙的技巧,我不得不承认这很酷。然而,上面的函数仍然很好地演示了如何在一般情况下处理一些标准库调用的加速问题。

可能是这个代码会更快:

char *begin = strrchr(buf, ' ');
atoi(begin ? begin : buf);

假设快速平台优化了标准函数strrchr。