Использование ThreadStatic для замены дорогостоящих локальных жителей - хорошая идея?

Обновить: как и следовало ожидать, советы сообщества в ответ на этот вопрос состояли в том, чтобы "измерить и увидеть". chibacity отправил ответ с некоторыми действительно хорошими тестами, которые сделали это для меня; Между тем, я написал собственное испытание; и разница в производительности, которую я видел, была на самом деле настолько огромной, что Мне было необходимо написать сообщение в блоге об этом.

Тем не менее, я должен также признать объяснение Ганса, что атрибут ThreadStatic действительно не является бесплатным и на самом деле полагается на вспомогательный метод CLR для работы с его магией. Это делает далеко не очевидным, будет ли подходящая оптимизация применяться в любом произвольном случае.

Хорошей новостью для меня является то, что в моем случае это, похоже, значительно улучшилось.


У меня есть метод, который (среди многих других) создает несколько массивов среднего размера (~ 50 элементов) для нескольких локальных переменных.

После некоторого профилирования я определил этот метод как нечто узкое место в производительности. Дело не в том, что метод требует очень долгого времени; Скорее, его просто называют много раз, очень быстро (от сотен тысяч до миллионов раз в сеансе, который будет длиться несколько часов). Поэтому даже относительно небольшие улучшения в его производительности должны быть полезными.

Мне пришло в голову, что, возможно, вместо выделения нового массива для каждого вызова я мог бы использовать поля с пометкой [ThreadStatic]; всякий раз, когда вызывается метод, он проверяет, инициализировано ли поле в текущем потоке, а если нет, инициализируйте его. С этого момента все вызовы в том же потоке будут иметь массив, готовый идти в этой точке.

(Метод инициализирует каждый элемент в самом массиве, поэтому наличие "устаревших" элементов в массиве не должно быть проблемой.)

Мой вопрос - это просто: это кажется хорошей идеей? Есть ли проблемы с использованием атрибута ThreadStatic таким образом (т.е. Как оптимизация производительности для уменьшения стоимости создания экземпляров новых объектов для локальных переменных), о которых я должен знать? Возможно ли, что производительность поля ThreadStatic невелика; например, существует ли много лишних "вещей" в фоновом режиме с собственным набором затрат, чтобы сделать эту функцию возможной?

Мне также кажется вполне правдоподобным, что я даже пытаюсь оптимизировать что-то дешевое (?) как массив из 50 элементов, и если это так, определенно дайте мне знать, но общий вопрос все еще сохраняется.

Ответы

Ответ 1

Я выполнил простой тест и ThreadStatic работает лучше для простых параметров, описанных в вопросе.

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

Обновление

С тестами, которые включают добавленную итерацию массива для моделирования использования минимального массива, плюс использование ссылочного массива ThreadStatic в дополнение к предыдущему тесту, где ссылка была скопирована локально:

Iterations : 10,000,000

Local ArrayRef          (- array iteration) : 330.17ms
Local ArrayRef          (- array iteration) : 327.03ms
Local ArrayRef          (- array iteration) : 1382.86ms
Local ArrayRef          (- array iteration) : 1425.45ms
Local ArrayRef          (- array iteration) : 1434.22ms
TS    CopyArrayRefLocal (- array iteration) : 107.64ms
TS    CopyArrayRefLocal (- array iteration) : 92.17ms
TS    CopyArrayRefLocal (- array iteration) : 92.42ms
TS    CopyArrayRefLocal (- array iteration) : 92.07ms
TS    CopyArrayRefLocal (- array iteration) : 92.10ms
Local ArrayRef          (+ array iteration) : 1740.51ms
Local ArrayRef          (+ array iteration) : 1647.26ms
Local ArrayRef          (+ array iteration) : 1639.80ms
Local ArrayRef          (+ array iteration) : 1639.10ms
Local ArrayRef          (+ array iteration) : 1646.56ms
TS    CopyArrayRefLocal (+ array iteration) : 368.03ms
TS    CopyArrayRefLocal (+ array iteration) : 367.19ms
TS    CopyArrayRefLocal (+ array iteration) : 367.22ms
TS    CopyArrayRefLocal (+ array iteration) : 368.20ms
TS    CopyArrayRefLocal (+ array iteration) : 367.37ms
TS    TSArrayRef        (+ array iteration) : 360.45ms
TS    TSArrayRef        (+ array iteration) : 359.97ms
TS    TSArrayRef        (+ array iteration) : 360.48ms
TS    TSArrayRef        (+ array iteration) : 360.03ms
TS    TSArrayRef        (+ array iteration) : 359.99ms

код:

[ThreadStatic]
private static int[] _array;

[Test]
public object measure_thread_static_performance()
{
    const int TestIterations = 5;
    const int Iterations = (10 * 1000 * 1000);
    const int ArraySize = 50;

    Action<string, Action> time = (name, test) =>
    {
        for (int i = 0; i < TestIterations; i++)
        {
            TimeSpan elapsed = TimeTest(test, Iterations);
            Console.WriteLine("{0} : {1:F2}ms", name, elapsed.TotalMilliseconds);
        }
    };

    int[] array = null;
    int j = 0;

    Action test1 = () =>
    {
        array = new int[ArraySize];
    };

    Action test2 = () =>
    {
        array = _array ?? (_array = new int[ArraySize]);
    };

    Action test3 = () =>
    {
        array = new int[ArraySize];

        for (int i = 0; i < ArraySize; i++)
        {
            j = array[i];
        }
    };

    Action test4 = () =>
    {
        array = _array ?? (_array = new int[ArraySize]);

        for (int i = 0; i < ArraySize; i++)
        {
            j = array[i];
        }
    };

    Action test5 = () =>
    {
        array = _array ?? (_array = new int[ArraySize]);

        for (int i = 0; i < ArraySize; i++)
        {
            j = _array[i];
        }
    };

    Console.WriteLine("Iterations : {0:0,0}\r\n", Iterations);
    time("Local ArrayRef          (- array iteration)", test1);
    time("TS    CopyArrayRefLocal (- array iteration)", test2);
    time("Local ArrayRef          (+ array iteration)", test3);
    time("TS    CopyArrayRefLocal (+ array iteration)", test4);
    time("TS    TSArrayRef        (+ array iteration)", test5);

    Console.WriteLine(j);

    return array;
}

[SuppressMessage("Microsoft.Reliability", "CA2001:AvoidCallingProblematicMethods", MessageId = "System.GC.Collect")]
private static TimeSpan TimeTest(Action action, int iterations)
{
    Action gc = () =>
    {
        GC.Collect();
        GC.WaitForFullGCComplete();
    };

    Action empty = () => { };

    Stopwatch stopwatch1 = Stopwatch.StartNew();

    for (int j = 0; j < iterations; j++)
    {
        empty();
    }

    TimeSpan loopElapsed = stopwatch1.Elapsed;

    gc();
    action(); //JIT
    action(); //Optimize

    Stopwatch stopwatch2 = Stopwatch.StartNew();

    for (int j = 0; j < iterations; j++) action();

    gc();

    TimeSpan testElapsed = stopwatch2.Elapsed;

    return (testElapsed - loopElapsed);
}

Ответ 2

[ThreadStatic] - бесплатный обед. Каждый доступ к переменной должен проходить через вспомогательную функцию в CLR (JIT_GetThreadFieldAddr_Primitive/Objref) вместо того, чтобы скомпилировать встроенный джиттер. Это также не является заменой локальной переменной, рекурсия идет в байты. Вам действительно нужно прокомментировать это, пытаясь перенести с тем, что код CLR в цикле невозможен.

Ответ 3

Из результатов, таких как this, ThreadStatic выглядит довольно быстро. Я не уверен, что у кого-то есть конкретный ответ, если он быстрее, чем перераспределение 50-элементного массива. Это то, что вам нужно, чтобы сравнить себя.:)

Я немного разорван, если это "хорошая идея" или нет. Пока все детали реализации хранятся внутри класса, это не обязательно плохая идея (вы действительно не хотите, чтобы вызывающий пользователь должен был беспокоиться об этом), но если контрольные показатели не показали прирост производительности от этого метода, я бы придерживался просто каждый раз выделяя массив, потому что он упрощает и упрощает чтение кода. Как более сложный из двух решений, мне нужно будет увидеть некоторую выгоду от сложности, прежде чем выбирать этот.