如何在C#中将int转换为char[]而不生成垃圾

6
毫无疑问,鉴于ToString()Convert.ToString()的可用性,这似乎是一个奇怪的请求,但我需要将无符号整数(即UInt32)转换为其字符串表示形式,并将答案存储到char[]中。
原因是为了效率而使用字符数组,由于目标char[]在对象创建时被初始化为一个成员char[10](用于保存UInt32.MaxValue的字符串表示形式),因此理论上可能实��转换不生成任何垃圾(我的意思是不生成托管堆中的任何临时对象)。
有人能想到一种简洁的方法来实现这个吗? (我在Framework 3.5SP1中工作,如果有任何相关信息,请告诉我。)

3
使用取模和除法手动进行转换。 - Matt Greer
2
你确定有足够的性能开销会影响你的应用程序吗?int.MaxValue 最多只有30多个数字,因此偶尔创建和垃圾回收的30个字符的字符串几乎不会被注意到。 - jonathanpeppers
2
这听起来很像过早优化。你确定你真的需要使用那些char[]而不是字符串吗?通常,为了获得一个可行的实现所需的额外工作并不值得性能提升(如果有的话——在.NET中分配和收集短暂的小对象非常便宜)。 - Lucero
2
马特 - 尊重地说,那是相当明显的 - 我正在寻找一个简洁的实现。乔纳森W - 换句话说,没有临时字符串。乔纳森P,卢塞罗 - 你们可能是对的,但我发现 StackOverflow 上让我沮丧的一件事是人们经常跳上过早优化的车,而真正的答案可能确实相关。也许我应该表达得更清楚,但我仍然感谢任何花时间发布任何类型答案的人。 - paytools-steve
2
@Steve,我已经发布了一个答案 - 但是在这种情况下,关于过早优化的问题有些必要,因为即使对于您的情况不是这样,对于在进行Google搜索后阅读此问题的其他人来说,可能也是如此。 - Lucero
显示剩余3条评论
3个回答

5

针对我上面的评论,我想知道log10是否太慢了,所以我编写了一个不使用它的版本。

对于四位数,这个版本大约快了35%,对于十位数则快了约16%。

一个缺点是它需要缓冲区中完整的十个数字的空间。

我不能保证它没有任何错误!

public static int ToCharArray2(uint value, char[] buffer, int bufferIndex)
{
    const int maxLength = 10;

    if (value == 0)
    {
        buffer[bufferIndex] = '0';
        return 1;
    }

    int startIndex = bufferIndex + maxLength - 1;
    int index = startIndex;
    do
    {
        buffer[index] = (char)('0' + value % 10);
        value /= 10;
        --index;
    }
    while (value != 0);

    int length = startIndex - index;

    if (bufferIndex != index + 1)
    {
        while (index != startIndex)
        {
            ++index;
            buffer[bufferIndex] = buffer[index];
            ++bufferIndex;
        }
    }

    return length;
}

更新

我应该补充一下,我正在使用 Pentium 4 处理器。更近期的处理器可能会更快地计算超越函数。

结论

昨天我意识到自己犯了一个低级错误,并在调试版本上运行基准测试。所以我又跑了一遍,但实际上并没有太大的区别。第一列显示要转换的数字的位数。其余列显示将 500,000 个数字转换为毫秒的时间。

uint 的结果:

    luc1   arx henk1  luc3 henk2  luc2
 1   715   217   966   242   837   244
 2   877   420  1056   541   996   447
 3  1059   608  1169   835  1040   610
 4  1184   795  1282  1116  1162   801
 5  1403   969  1405  1396  1279   978
 6  1572  1149  1519  1674  1399  1170
 7  1740  1335  1648  1952  1518  1352
 8  1922  1675  1868  2233  1750  1545
 9  2087  1791  2005  2511  1893  1720
10  2263  2103  2139  2797  2012  1985

Results for ulong:

    luc1   arx henk1  luc3 henk2  luc2
 1   802   280   998   390   856   317
 2   912   516  1102   729   954   574
 3  1066   746  1243  1060  1056   818
 4  1300  1141  1362  1425  1170  1210
 5  1557  1363  1503  1742  1306  1436
 6  1801  1603  1612  2233  1413  1672
 7  2269  1814  1723  2526  1530  1861
 8  2208  2142  1920  2886  1634  2149
 9  2360  2376  2063  3211  1775  2339
10  2615  2622  2213  3639  2011  2697
11  3048  2996  2513  4199  2244  3011
12  3413  3607  2507  4853  2326  3666
13  3848  3988  2663  5618  2478  4005
14  4298  4525  2748  6302  2558  4637
15  4813  5008  2974  7005  2712  5065
16  5161  5654  3350  7986  2994  5864
17  5997  6155  3241  8329  2999  5968
18  6490  6280  3296  8847  3127  6372
19  6440  6720  3557  9514  3386  6788
20  7045  6616  3790 10135  3703  7268

luc1: 卢塞罗的第一个函数

arx: 我的函数

henk1: 亨克的函数

luc3 卢塞罗的第三个函数

henk2: 亨克的函数没有复制到字符数组;即只测试ToString()的性能。

luc2: 卢塞罗的第二个函数

这种特殊顺序是它们创建的顺序。

我还在没有henk1和henk2的情况下运行了测试,这样就不会有垃圾回收。另外三个函数的时间几乎相同。一旦基准超过三位数,内存使用率就稳定了: 因此,在Henk的函数中发生GC,并且对其他函数没有不利影响。

结论:只需调用ToString()


我编辑了我的解决方案,并添加了两个不使用超越函数的变体。 - Lucero
考虑到您的声望,显然您是一个相对新手,所以我真的希望我能做更多的事情来回报您,但Lucero的答案现在已经非常完整了,我认为它应该被标记为正确答案。尽管如此,在进行大量应用程序微秒计数时(有时在我的某些应用程序中确实需要),您的函数在最多3位数字上显然是赢家,非常感谢您进行所有这些基准测试。很有趣看到这种优势随着数字变得越来越长而逐渐减弱。非常感谢! - paytools-steve
由于@arx在基准测试方面的辛勤工作,此次被接受。请也查看@Lucero的答案,其中有一些很好的见解。感谢所有贡献者的努力:-)。 - paytools-steve

3
下面的代码可以实现此功能,但有一个警告:它不遵循文化设置,而总是输出普通十进制数字。
public static int ToCharArray(uint value, char[] buffer, int bufferIndex) {
    if (value == 0) {
        buffer[bufferIndex] = '0';
        return 1;
    }
    int len = (int)Math.Ceiling(Math.Log10(value));
    for (int i = len-1; i>= 0; i--) {
        buffer[bufferIndex+i] = (char)('0'+(value%10));
        value /= 10;
    }
    return len;
}

返回值是已使用的char[]数量。 编辑(针对arx):以下版本避免了浮点数运算,并在原地交换缓冲区:
public static int ToCharArray(uint value, char[] buffer, int bufferIndex) {
    if (value == 0) {
        buffer[bufferIndex] = '0';
        return 1;
    }
    int bufferEndIndex = bufferIndex;
    while (value > 0) {
        buffer[bufferEndIndex++] = (char)('0'+(value%10));
        value /= 10;
    }
    int len = bufferEndIndex-bufferIndex;
    while (--bufferEndIndex > bufferIndex) {
        char ch = buffer[bufferEndIndex];
        buffer[bufferEndIndex] = buffer[bufferIndex];
        buffer[bufferIndex++] = ch;
    }
    return len;
}

这里是另一种计算小循环中数字数量的变体:

public static int ToCharArray(uint value, char[] buffer, int bufferIndex) {
    if (value == 0) {
        buffer[bufferIndex] = '0';
        return 1;
    }
    int len = 1;
    for (uint rem = value/10; rem > 0; rem /= 10) {
        len++;
    }
    for (int i = len-1; i>= 0; i--) {
        buffer[bufferIndex+i] = (char)('0'+(value%10));
        value /= 10;
    }
    return len;
}

我把基准测试留给想做的人... ;)


1
这假设缓冲区已经被初始化为所有0。 - Chris Pitman
Lucero - 这正是我所想的(现在我为催促您进行过早优化而感到难过!)。Math.Ceiling(Math.Log10(value)) 就是我缺少的技巧。非常感谢 :-). (另外,我不关心文化设置,我只想要一组原始数字)。 - paytools-steve
@Steve,不用谢。我总是尽力回答问题,以便其他人在阅读时能够清楚地了解范围。@Chris,我不会假设数组的任何初始化;但我只会修改所需的字符数量,这就是为什么我返回长度的原因(如果您想要一个以零结尾的字符数组,那么也可以使用它来添加'\0')。 - Lucero
从前,log10是一项非常昂贵的操作,生成缓冲区末尾的数字并将它们向前移动比直接生成更快。我不知道现在是否仍然如此,但如果速度真的很重要,那么测试一下可能是值得的。 - arx
@Lucero - 缓冲区交换版本在处理数字位数为偶数的情况下会得出错误的答案。while循环中的>=应该改为>。 - arx
显示剩余3条评论

0

我来晚了一点,但我猜你可能无法获得比简单的内存重新解释更快且占用更少内存的结果:

    [System.Security.SecuritySafeCritical]
    public static unsafe char[] GetChars(int value, char[] chars)
    {
        //TODO: if needed to use accross machines then
        //  this should also use BitConverter.IsLittleEndian to detect little/big endian
        //  and order bytes appropriately

        fixed (char* numPtr = chars)
            *(int*)numPtr = value;
        return chars;
    }

    [System.Security.SecuritySafeCritical]
    public static unsafe int ToInt32(char[] value)
    {
        //TODO: if needed to use accross machines then
        //  this should also use BitConverter.IsLittleEndian to detect little/big endian
        //  and order bytes appropriately

        fixed (char* numPtr = value)
            return *(int*)numPtr;
    }

这只是一个想法的演示 - 显然,您需要添加字符数组大小检查并确保您具有适当的字节顺序编码。您可以查看BitConverter的反射辅助方法进行这些检查。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接