Имитация разрывания двойного в C # - PullRequest
18 голосов
/ 25 января 2012

Я работаю на 32-разрядной машине и могу подтвердить, что длинные значения могут порваться, используя следующий фрагмент кода, который очень быстро срабатывает.

        static void TestTearingLong()
        {
            System.Threading.Thread A = new System.Threading.Thread(ThreadA);
            A.Start();

            System.Threading.Thread B = new System.Threading.Thread(ThreadB);
            B.Start();
        }

        static ulong s_x;

        static void ThreadA()
        {
            int i = 0;
            while (true)
            {
                s_x = (i & 1) == 0 ? 0x0L : 0xaaaabbbbccccddddL;
                i++;
            }
        }

        static void ThreadB()
        {
            while (true)
            {
                ulong x = s_x;
                Debug.Assert(x == 0x0L || x == 0xaaaabbbbccccddddL);
            }
        }

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

    static double s_x;

    static void TestTearingDouble()
    {
        System.Threading.Thread A = new System.Threading.Thread(ThreadA);
        A.Start();

        System.Threading.Thread B = new System.Threading.Thread(ThreadB);
        B.Start();
    }

    static void ThreadA()
    {
        long i = 0;

        while (true)
        {
            s_x = ((i & 1) == 0) ? 0.0 : double.MaxValue;
            i++;

            if (i % 10000000 == 0)
            {
                Console.Out.WriteLine("i = " + i);
            }
        }
    }

    static void ThreadB()
    {
        while (true)
        {
            double x = s_x;

            System.Diagnostics.Debug.Assert(x == 0.0 || x == double.MaxValue);
        }
    }

Ответы [ 4 ]

12 голосов
/ 25 января 2012

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

РЕДАКТИРОВАТЬ: узнал, что трудный путь несколько лет назад.

11 голосов
/ 29 января 2012
static double s_x;

Гораздо сложнее продемонстрировать эффект при использовании дубля. Процессор использует специальные инструкции для загрузки и хранения двойных, соответственно, FLD и FSTP. С long это намного проще, так как нет ни одной инструкции, которая загружает / сохраняет 64-битное целое число в 32-битном режиме. Для этого необходимо, чтобы адрес переменной был выровнен, чтобы он не перекрывал границу строки кэша процессора.

Этого никогда не случится с объявлением, которое вы использовали, JIT-компилятор гарантирует, что double выровнен правильно, сохранен по адресу, кратному 8. Вы можете сохранить его в поле класса, распределитель GC только выравнивает до 4 в 32-битном режиме. Но это дерьмовый выстрел.

Лучший способ сделать это - преднамеренно неправильно выровнять двойное число с помощью указателя. Поместите unsafe перед классом Program и сделайте так, чтобы он выглядел примерно так:

    static double* s_x;

    static void Main(string[] args) {
        var mem = Marshal.AllocCoTaskMem(100);
        s_x = (double*)((long)(mem) + 28);
        TestTearingDouble();
    }
ThreadA:
            *s_x = ((i & 1) == 0) ? 0.0 : double.MaxValue;
ThreadB:
            double x = *s_x;

Это по-прежнему не гарантирует хорошего смещения (хе-хе), поскольку нет способа точно определить, где AllocCoTaskMem () будет выравнивать распределение относительно начала строки кэша процессора. И это зависит от ассоциативности кэша в вашем ядре процессора (у меня это Core i5). Вам придется возиться со смещением, я получил значение 28 экспериментальным путем. Значение должно делиться на 4, но не на 8, чтобы действительно имитировать поведение кучи GC. Продолжайте добавлять 8 к значению до тех пор, пока не получите двойное, чтобы расположить строку кэша и вызвать утверждение.

Чтобы сделать его менее искусственным, вам нужно написать программу, которая хранит значение типа double в поле класса и заставить сборщик мусора перемещать его в памяти, чтобы он не выравнивался. В некотором роде трудно придумать пример программы, которая гарантирует , что это происходит.

Также обратите внимание, как ваша программа может продемонстрировать проблему, называемую false share . Закомментируйте вызов метода Start () для потока B и отметьте, насколько быстрее работает поток A. Вы видите стоимость процессора, поддерживающего согласованность строки кэша между ядрами процессора. Обмен предназначен здесь, так как потоки обращаются к одной и той же переменной. Реальное ложное совместное использование происходит, когда потоки обращаются к различным переменным, которые хранятся в одной и той же строке кэша. В противном случае выравнивание имеет значение: вы можете наблюдать разрыв для двойника только тогда, когда его часть находится в одной строке кэша, а часть - в другой.

0 голосов
/ 29 января 2012

Для чего стоит эта тема и пример кода можно найти здесь.

http://msdn.microsoft.com/en-us/magazine/cc817398.aspx

0 голосов
/ 29 января 2012

Выполняя некоторые раскопки, я нашел несколько интересных чтений, касающихся операций с плавающей запятой на архитектурах x86:

Согласно Википедии , модуль x86 с плавающей запятой хранит значения с плавающей запятой в 80-битных регистрах:

[...] последующие процессоры x86 затем интегрировали эту функциональность x87 на чипе, который сделал инструкции x87 де-факто неотъемлемой частью набор инструкций x86. Каждый регистр x87, известный как ST (0) через ST (7), имеет ширину 80 битов и хранит числа в плавающей точке IEEE стандартный двойной расширенный формат точности.

Также связан этот другой SO вопрос: Некоторые вопросы о точности с плавающей запятой и числовых пределах

Это может объяснить, почему, хотя double являются 64-битными, они работают атомарно.

...