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


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

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

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

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

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

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

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

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

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

3 15

3 ответа:

Я провел простой тест и 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);
}

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

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

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