Почему "dtoa.c" содержит столько кода?
Я буду первым, кто признает, что мои общие знания о программировании на низком уровне немного редки. Я понимаю многие основные понятия, но я не использую их на регулярной основе.
Это было сказано, что я был совершенно поражен тем, сколько кода понадобилось для dtoa.c.
За последние пару месяцев я работал над внедрением ECMAScript на С#, и я замедлял заполнение отверстий в моем движке. Прошлой ночью я начал работать с Number.prototype.toString, который описан в разделе 15.7.4.2 Спецификация ECMAScript (pdf). В разделе 9.8.1, ПРИМЕЧАНИЕ 3 предлагает ссылку на dtoa.c, но я искал вызов, поэтому я ждал его просмотра. Ниже приводится то, что я придумал.
private IDynamic ToString(Engine engine, Args args)
{
var thisBinding = engine.Context.ThisBinding;
if (!(thisBinding is NumberObject) && !(thisBinding is NumberPrimitive))
{
throw RuntimeError.TypeError("The current 'this' must be a number or a number object.");
}
var num = thisBinding.ToNumberPrimitive();
if (double.IsNaN(num))
{
return new StringPrimitive("NaN");
}
else if (double.IsPositiveInfinity(num))
{
return new StringPrimitive("Infinity");
}
else if (double.IsNegativeInfinity(num))
{
return new StringPrimitive("-Infinity");
}
var radix = !args[0].IsUndefined ? args[0].ToNumberPrimitive().Value : 10D;
if (radix < 2D || radix > 36D)
{
throw RuntimeError.RangeError("The parameter [radix] must be between 2 and 36.");
}
else if (radix == 10D)
{
return num.ToStringPrimitive();
}
var sb = new StringBuilder();
var isNegative = false;
if (num < 0D)
{
isNegative = true;
num = -num;
}
var integralPart = Math.Truncate(num);
var decimalPart = (double)((decimal)num.Value - (decimal)integralPart);
var radixChars = RadixMap.GetArray((int)radix);
if (integralPart == 0D)
{
sb.Append('0');
}
else
{
var integralTemp = integralPart;
while (integralTemp > 0)
{
sb.Append(radixChars[(int)(integralTemp % radix)]);
integralTemp = Math.Truncate(integralTemp / radix);
}
}
var count = sb.Length - 1;
for (int i = 0; i < count; i++)
{
var k = count - i;
var swap = sb[i];
sb[i] = sb[k];
sb[k] = swap;
}
if (isNegative)
{
sb.Insert(0, '-');
}
if (decimalPart == 0D)
{
return new StringPrimitive(sb.ToString());
}
var runningValue = 0D;
var decimalIndex = 1D;
var decimalTemp = decimalPart;
sb.Append('.');
while (decimalIndex < 100 && decimalPart - runningValue > 1.0e-50)
{
var result = decimalTemp * radix;
var integralResult = Math.Truncate(result);
runningValue += integralResult / Math.Pow(radix, decimalIndex++);
decimalTemp = result - integralResult;
sb.Append(radixChars[(int)integralResult]);
}
return new StringPrimitive(sb.ToString());
}
Может ли кто-нибудь, у кого больше опыта программирования на низком уровне, объяснить, почему dtoa.c имеет примерно в 40 раз больше кода? Я просто не могу представить, чтобы С# был намного более продуктивным.
Ответы
Ответ 1
dtoa.c содержит две основные функции: dtoa(), которая преобразует double в строку и strtod(), которая преобразует строку в double. Он также содержит множество вспомогательных функций, большинство из которых предназначены для собственной реализации арифметики произвольной точности. Требование dtoa.c к славе приводит к правильным преобразованиям, и это может быть сделано только в случае арифметики с произвольной точностью. Он также имеет код для округлых преобразований правильно в четырех разных режимах округления.
Ваш код пытается реализовать эквивалент dtoa(), и поскольку он использует плавающие точки для выполнения своих преобразований, они не всегда будут корректными. (Обновление: подробности см. В моей статье http://www.exploringbinary.com/quick-and-dirty-floating-point-to-decimal-conversion/.)
(Я много писал об этом в своем блоге http://www.exploringbinary.com/. Шесть из моих последних семи статей касались strtod (). Прочитайте их, чтобы понять, насколько сложно делать правильно округленные преобразования.)
Ответ 2
Получение хороших результатов для конверсий между десятичными и двоичными представлениями с плавающей запятой является довольно сложной задачей.
Основной трудностью является то, что многие десятичные дроби, даже простые, не могут быть точно выражены с помощью двоичной с плавающей запятой - например, 0.5
может (очевидно), но 0.1
не может. И, идя в другую сторону (от двоичного до десятичного), вы обычно не хотите абсолютно точного результата (например, точное десятичное значение ближайшего номера до 0.1
, которое может быть представлено в IEEE-754-совместимом double
на самом деле
0.1000000000000000055511151231257827021181583404541015625
), поэтому вы обычно хотите округлить.
Таким образом, преобразование часто связано с приближением. Хорошие процедуры преобразования гарантируют получение максимально возможного приближения в пределах определенных ограничений (размер слова или количество цифр). Именно здесь происходит большая часть сложности.
Взгляните на статью, процитированную в комментарии в верхней части реализации dtoa.c
, Clinger Как точно читать числа с плавающей запятой, для аромата проблемы; и, возможно, документ Дэвида М. Гей (автора), Правильно округленные двоично-десятичные и десятичные двоичные преобразования.
(Кроме того, в более общем плане: Что каждый компьютерный ученый должен знать о арифметике с плавающей точкой.)
Ответ 3
Основываясь на быстром взгляде на это, значительная часть версии C имеет дело с несколькими платформами, и похоже, что этот файл предназначен для универсального использования для всех компиляторов (C и С++), битнес, реализаций с плавающей запятой, и платформы; с тоннами #define
конфигурируемости.
Ответ 4
Я также считаю, что код в dtoa.c может быть более эффективным (независимо от языка). Например, кажется, что он делает бит-ворчание, которое в руках эксперта часто означает скорость. Я предполагаю, что он просто использует менее интуитивный алгоритм по причинам скорости.
Ответ 5
Короткий ответ: потому что dtoa.c
работает.
Это как раз разница между хорошо отлаженным продуктом и прототипом NIH.