你知道如何将浮点数转换为字符缓冲区,而不分配任何内存吗?
=>我只想重做与float.ToString()相同的事情;所以我可以将结果放入缓冲区而不是分配字符串
我写了一个函数,但它没有很好地处理“舍入”:
那是因为39.71作为浮点数是内存中存储值的舍入值39.709996。通过我的函数中的一些舍入,我可以很容易地得到这样的结果:
这也不是很好,因为我想保持float.ToString()
完全相同的算法来设法写“39.71”和“39.71001”
你知道这个float.ToString()
是如何运作的吗?
我的目标的精确度:我想在一个非常大的字符串中添加大量浮点数(与其他类型混合) - 并且在结尾处仅分配一次该字符串以避免过多的垃圾收集。所以我真的需要将float转换为char数组(无论确切的类型格式,只是没有不可变的字符串)
答案 0 :(得分:2)
您可以调用C stdlib sprintf()
和朋友来解决大多数格式和数据问题。有关示例,请参阅https://stackoverflow.com/a/2479210/3150802。通过评估sprintf()
的返回值来跟踪打印位置(即char数组的索引)将非常重要。
跨越托管/非托管边界会有一些性能损失,这是不可避免的。可能的缓解策略是减少此类交叉的数量,例如通过编写C包装函数,该函数接收大量浮点数并使用s*printf()
一次性写入它们。
数据编组的潜在损失(即CLI和本机表示之间的来回转换)可能不是浮点数和地址等POD的问题,如果它们在两个世界中都是逐位相同的。
答案 1 :(得分:1)
从您的评论中看来,您希望能够将大量浮点数(和其他值类型)格式化为非常大的字符数组,而无需进行大量内存分配。
无法避免所有内存分配,但您可以使用StringBuilder.AppendFormat()
和StringBuilder.CopyTo()
来模仿它们。
如果您对最终字符串数组的最大长度有所了解,则可以初始化StringBuilder
,其容量足以容纳它。这可能会减少内存分配的数量,但如果使缓冲区太大,则会浪费内存。
代码是这样的:
int capacity = 8192; // If you have some idea of the final string length.
var sb = new StringBuilder(capacity);
for (float x = 0; x < 1000; x += 1.2345f)
sb.AppendFormat(", {0}", x);
char[] array = new char[sb.Length];
sb.CopyTo(0, array, 0, sb.Length); // Now array[] contains the result.
请注意,此处 buffer 分配的最小数量为2:一个用于StringBuilder
使用的内部缓冲区,另一个用于char []数组。
然而,很可能会在幕后发生许多其他小规模的分配 - 但由于GC的工作方式,它们不太可能进入任何第1代集合,因此它不太可能导致性能问题。
答案 2 :(得分:1)
以下是我最后编写的解决方案,这要归功于Marc Gravell提供的float.ToString()源代码。
它已经大大简化了,但现在似乎工作得很好(也许我错过了一些特殊情况)。 我看到的唯一主要区别是像5.34E-05这样的非常小的浮点数基本上会写成0.0000534(实际上我更喜欢这样)
这里的信息是我的完整StringFast类(Unity C#代码):http://pastebin.com/HqAw2pTG。以及在此使用它的一些测试:http://pastebin.com/brynBFyC
测试运行1000次这些操作:4次追加(2个字符串,一个浮点数和一个int)和1个字符串替换。我使用带有+和Concat()的字符串,使用StringBuilder和使用我的StringFast类来运行测试。当然,对于最后两个,我不会每次都重新创建它们。
以下是结果,内存分配和时间:
如果我用100个浮点数串联替换我的5个操作:
正如您所看到的,StringBuilder并不是那么好,特别是当字符串上只有几个操作时。 StringFast类的分配只是由我做的最终ToString()引起的(为了能够将字符串与其他函数一起使用)
以下是浮点数转换为char数组的代码:
///<summary>Append a float without memory allocation.</summary>
public StringFast Append( float valueF )
{
double value = valueF;
m_isStringGenerated = false;
ReallocateIFN( 32 ); // Check we have enough buffer allocated to handle any float number
// Handle the 0 case
if( value == 0 )
{
m_buffer[ m_bufferPos++ ] = '0';
return this;
}
// Handle the negative case
if( value < 0 )
{
value = -value;
m_buffer[ m_bufferPos++ ] = '-';
}
// Get the 7 meaningful digits as a long
int nbDecimals = 0;
while( value < 1000000 )
{
value *= 10;
nbDecimals++;
}
long valueLong = (long)System.Math.Round( value );
// Parse the number in reverse order
int nbChars = 0;
bool isLeadingZero = true;
while( valueLong != 0 || nbDecimals >= 0 )
{
// We stop removing leading 0 when non-0 or decimal digit
if( valueLong%10 != 0 || nbDecimals <= 0 )
isLeadingZero = false;
// Write the last digit (unless a leading zero)
if( !isLeadingZero )
m_buffer[ m_bufferPos + (nbChars++) ] = (char)('0' + valueLong%10);
// Add the decimal point
if( --nbDecimals == 0 && !isLeadingZero )
m_buffer[ m_bufferPos + (nbChars++) ] = '.';
valueLong /= 10;
}
m_bufferPos += nbChars;
// Reverse the result
for( int i=nbChars/2-1; i>=0; i-- )
{
char c = m_buffer[ m_bufferPos-i-1 ];
m_buffer[ m_bufferPos-i-1 ] = m_buffer[ m_bufferPos-nbChars+i ];
m_buffer[ m_bufferPos-nbChars+i ] = c;
}
return this;
}