Nano Hash - криптовалюты, майнинг, программирование

Арифметические вычисления с использованием Linq.Expressions дают разные результаты для 32- и 64-битных систем.

Я наблюдаю какое-то странное поведение в отношении результатов следующего кода:

namespace Test {
  class Program {
    private static readonly MethodInfo Tan = typeof(Math).GetMethod("Tan", new[] { typeof(double) });
    private static readonly MethodInfo Log = typeof(Math).GetMethod("Log", new[] { typeof(double) });

    static void Main(string[] args) {
    var c1 = 9.97601998143507984195821336470544338226318359375d;
    var c2 = -0.11209109500765944422706610339446342550218105316162109375d;

    var result1 = Math.Pow(Math.Tan(Math.Log(c1) / Math.Tan(c2)), 2);

    var p1 = Expression.Parameter(typeof(double));
    var p2 = Expression.Parameter(typeof(double));
    var expr = Expression.Power(Expression.Call(Tan, Expression.Divide(Expression.Call(Log, p1), Expression.Call(Tan, p2))), Expression.Constant(2d));
    var lambda = Expression.Lambda<Func<double, double, double>>(expr, p1, p2);
    var result2 = lambda.Compile()(c1, c2);

    var s1 = DoubleConverter.ToExactString(result1);
    var s2 = DoubleConverter.ToExactString(result2);

    Console.WriteLine("Result1: {0}", s1);
    Console.WriteLine("Result2: {0}", s2);
  }
}

Код, скомпилированный для x64, дает тот же результат:

Result1: 4888.95508254035303252749145030975341796875
Result2: 4888.95508254035303252749145030975341796875

Но при компиляции для x86 или Any Cpu результаты отличаются:

Result1: 4888.95508254035303252749145030975341796875
Result2: 4888.955082542781383381225168704986572265625

Почему result1 остается неизменным, а result2 зависит от целевой архитектуры? Есть ли способ сделать так, чтобы result1 и result2 оставались одинаковыми в одной и той же архитектуре?

Класс DoubleConverter взят с сайта http://jonskeet.uk/csharp/DoubleConverter.cs. Прежде чем вы скажете мне использовать decimal, мне не нужно больше точности, мне просто нужно, чтобы результаты были последовательными. Целевой платформой является .NET 4.5.2, а тестовый проект был создан в режиме отладки. Я использую Visual Studio 2015 Update 1 RC в Windows 10.

Спасибо.

ИЗМЕНИТЬ

По предложению пользователя djcouchycouch я попытался еще больше упростить пример:

  var c1 = 9.97601998143507984195821336470544338226318359375d;
  var c2 = -0.11209109500765944422706610339446342550218105316162109375d;
  var result1 = Math.Log(c1) / Math.Tan(c2);
  var p1 = Expression.Parameter(typeof(double));
  var p2 = Expression.Parameter(typeof(double));
  var expr = Expression.Divide(Expression.Call(Log, p1), Expression.Call(Tan, p2));
  var lambda = Expression.Lambda<Func<double, double, double>>(expr, p1, p2);
  var result2 = lambda.Compile()(c1, c2);

x86 или AnyCpu, Отладка:

Result1: -20.43465311535924655572671326808631420135498046875
Result2: -20.434653115359243003013034467585384845733642578125

x64, Отладка:

Result1: -20.43465311535924655572671326808631420135498046875
Result2: -20.43465311535924655572671326808631420135498046875

x86 или AnyCpu, выпуск:

Result1: -20.434653115359243003013034467585384845733642578125
Result2: -20.434653115359243003013034467585384845733642578125

x64, выпуск:

Result1: -20.43465311535924655572671326808631420135498046875
Result2: -20.43465311535924655572671326808631420135498046875

Дело в том, что результаты варьируются между Debug, Release, x86 и x64, и чем сложнее формула, тем больше вероятность того, что она вызовет большие отклонения.


  • Я не могу воспроизвести эту проблему на VS2012 (Windows 7 SP1). Обе архитектуры дают одинаковый результат. 20.11.2015
  • Я вижу тот же результат, что и OP в Windows 7, VS2015 x86/Any CPU. 20.11.2015
  • Воспроизводится на Windows 7, VS2015. Результат зависит от конфигурации и подключенного отладчика. В режиме выпуска результат отличается при работе с отладчиком (F5) и без отладчика (Ctrl+F5). 20.11.2015
  • 32-битный JIT использует инструкции x87 и 80-битные временные результаты, а 64-битный JIT — нет. Но с инструкциями x87, если они временно сохраняются в памяти, они снова усекаются до 64-битных. Так что иногда бывает по-разному, но не обязательно, в зависимости от конкретного кодегена. 20.11.2015
  • Постарайтесь свести проблему к минимуму, который показывает проблему. Это лучше определит место, вызывающее разницу. 20.11.2015
  • Требуются ли для этого деревья выражений? Должен делать то же самое, если вы пишете его нормально (но не используя константы, которые компилятор может оптимизировать). 20.11.2015
  • Да, это часть проекта машинного обучения, в котором мы оцениваем математические выражения на основе дерева по набору данных, и использование скомпилированных выражений linq обеспечивает значительное улучшение скорости, когда данные достаточно велики. 20.11.2015
  • Нет, я имею в виду, возможно ли воспроизвести его только с помощью деревьев выражений? Я думаю, что они не связаны. 20.11.2015
  • Я думаю, что ответ Эрика Липперта на даже более простой пример заслуживает внимания: компилятор C#, джиттер и среда выполнения имеют широкую широту чтобы дать вам более точные результаты, чем требуется по спецификации, в любое время и по прихоти - они не обязаны делать это постоянно, и на самом деле они этого не делают. 20.11.2015

Ответы:


1

Это разрешено ECMA-335 I.12.1.3 Обработка типов данных с плавающей запятой:

[...] Места хранения для чисел с плавающей запятой (статика, элементы массива и поля классов) имеют фиксированный размер. Поддерживаемые размеры хранилища: float32 и float64. В других местах (в стеке вычислений, в качестве аргументов, в качестве возвращаемых типов и в качестве локальных переменных) числа с плавающей запятой представляются с использованием внутреннего типа с плавающей запятой. В каждом таком случае номинальным типом переменной или выражения является либо float32, либо float64, но ее значение может быть представлено внутри с дополнительным диапазоном и/или точностью. [...]

Как комментирует @harold ваш вопрос, это позволяет использовать 80-битные регистры FPU в режиме x86. Вот что происходит, когда оптимизация включена, то есть для вашего пользовательского кода, когда вы строите в режиме выпуска и не отлаживаете, но всегда для скомпилированных выражений.

Чтобы убедиться, что вы получаете последовательное округление, вам нужно хранить промежуточные результаты в поле или массиве. Это означает, что для надежного получения результатов для вашей версии, отличной от Expression, вам нужно написать что-то вроде:

var tmp = new double[2];
tmp[0] = Math.Log(c1);
tmp[1] = Math.Tan(c2);
tmp[0] /= tmp[1];
tmp[0] = Math.Tan(tmp[0]);
tmp[0] = Math.Pow(tmp[0], 2);

и тогда вы можете смело присваивать tmp[0] локальной переменной.

Да, это некрасиво.

Для версии Expression реальный синтаксис, который вам нужен, хуже, и я не буду его расписывать. Это включает в себя Expression.Block чтобы разрешить последовательное выполнение нескольких несвязанных подвыражений, Expression.Assign для назначения элементам или полям массива и доступа к этим элементам или полям массива.

20.11.2015
  • Когда JIT будет обновлен (если когда-либо) для оптимизации доступа к памяти (что сейчас позорно почти никогда не происходит), это сломается. Или доступ к массиву обеспечивает точную точность по спецификации? Я думаю, что избыточное приведение к float/double также сработает. 20.11.2015
  • @usr В спецификации, которую я цитировал, говорится, что поля и элементы массива могут не иметь дополнительной точности. Приведения не необходимы для снижения избыточной точности. 20.11.2015
  • Хорошо, тогда это работает. Можно также использовать struct { float F; } и вспомогательный метод, направляющий через эту структуру. 20.11.2015
  • Спасибо! Очень полезно. 20.11.2015
  • Новые материалы

    Кластеризация: более глубокий взгляд
    Кластеризация — это метод обучения без учителя, в котором мы пытаемся найти группы в наборе данных на основе некоторых известных или неизвестных свойств, которые могут существовать. Независимо от..

    Как написать эффективное резюме
    Предложения по дизайну и макету, чтобы представить себя профессионально Вам не позвонили на собеседование после того, как вы несколько раз подали заявку на работу своей мечты? У вас может..

    Частный метод Python: улучшение инкапсуляции и безопасности
    Введение Python — универсальный и мощный язык программирования, известный своей простотой и удобством использования. Одной из ключевых особенностей, отличающих Python от других языков, является..

    Как я автоматизирую тестирование с помощью Jest
    Шутка для победы, когда дело касается автоматизации тестирования Одной очень важной частью разработки программного обеспечения является автоматизация тестирования, поскольку она создает..

    Работа с векторными символическими архитектурами, часть 4 (искусственный интеллект)
    Hyperseed: неконтролируемое обучение с векторными символическими архитектурами (arXiv) Автор: Евгений Осипов , Сачин Кахавала , Диланта Хапутантри , Тимал Кемпития , Дасвин Де Сильва ,..

    Понимание расстояния Вассерштейна: мощная метрика в машинном обучении
    В обширной области машинного обучения часто возникает необходимость сравнивать и измерять различия между распределениями вероятностей. Традиционные метрики расстояния, такие как евклидово..

    Обеспечение масштабируемости LLM: облачный анализ с помощью AWS Fargate и Copilot
    В динамичной области искусственного интеллекта все большее распространение получают модели больших языков (LLM). Они жизненно важны для различных приложений, таких как интеллектуальные..