4

У меня возникли проблемы с пониманием того, почему моя «параллельная» реализация этой функции http://www.codeproject.com/Tips/447938/High-performance-Csharp-byte-array-to-hex-string-t имеет только прирост производительности ~ 20%.Почему Parallel.For дает лишь небольшое усиление для этой конкретной функции?

Для удобства здесь есть код с этого сайта:

static readonly int[] toHexTable = new int[] { 
    3145776, 3211312, 3276848, 3342384, 3407920, 3473456, 3538992, 3604528, 3670064, 3735600, 
    4259888, 4325424, 4390960, 4456496, 4522032, 4587568, 3145777, 3211313, 3276849, 3342385, 
    3407921, 3473457, 3538993, 3604529, 3670065, 3735601, 4259889, 4325425, 4390961, 4456497, 
    4522033, 4587569, 3145778, 3211314, 3276850, 3342386, 3407922, 3473458, 3538994, 3604530, 
    3670066, 3735602, 4259890, 4325426, 4390962, 4456498, 4522034, 4587570, 3145779, 3211315, 
    3276851, 3342387, 3407923, 3473459, 3538995, 3604531, 3670067, 3735603, 4259891, 4325427, 
    4390963, 4456499, 4522035, 4587571, 3145780, 3211316, 3276852, 3342388, 3407924, 3473460, 
    3538996, 3604532, 3670068, 3735604, 4259892, 4325428, 4390964, 4456500, 4522036, 4587572, 
    3145781, 3211317, 3276853, 3342389, 3407925, 3473461, 3538997, 3604533, 3670069, 3735605, 
    4259893, 4325429, 4390965, 4456501, 4522037, 4587573, 3145782, 3211318, 3276854, 3342390, 
    3407926, 3473462, 3538998, 3604534, 3670070, 3735606, 4259894, 4325430, 4390966, 4456502, 
    4522038, 4587574, 3145783, 3211319, 3276855, 3342391, 3407927, 3473463, 3538999, 3604535, 
    3670071, 3735607, 4259895, 4325431, 4390967, 4456503, 4522039, 4587575, 3145784, 3211320, 
    3276856, 3342392, 3407928, 3473464, 3539000, 3604536, 3670072, 3735608, 4259896, 4325432, 
    4390968, 4456504, 4522040, 4587576, 3145785, 3211321, 3276857, 3342393, 3407929, 3473465, 
    3539001, 3604537, 3670073, 3735609, 4259897, 4325433, 4390969, 4456505, 4522041, 4587577, 
    3145793, 3211329, 3276865, 3342401, 3407937, 3473473, 3539009, 3604545, 3670081, 3735617, 
    4259905, 4325441, 4390977, 4456513, 4522049, 4587585, 3145794, 3211330, 3276866, 3342402, 
    3407938, 3473474, 3539010, 3604546, 3670082, 3735618, 4259906, 4325442, 4390978, 4456514, 
    4522050, 4587586, 3145795, 3211331, 3276867, 3342403, 3407939, 3473475, 3539011, 3604547, 
    3670083, 3735619, 4259907, 4325443, 4390979, 4456515, 4522051, 4587587, 3145796, 3211332, 
    3276868, 3342404, 3407940, 3473476, 3539012, 3604548, 3670084, 3735620, 4259908, 4325444, 
    4390980, 4456516, 4522052, 4587588, 3145797, 3211333, 3276869, 3342405, 3407941, 3473477, 
    3539013, 3604549, 3670085, 3735621, 4259909, 4325445, 4390981, 4456517, 4522053, 4587589, 
    3145798, 3211334, 3276870, 3342406, 3407942, 3473478, 3539014, 3604550, 3670086, 3735622, 
    4259910, 4325446, 4390982, 4456518, 4522054, 4587590 
}; 

public static unsafe string ToHex1(byte[] source) 
{ 
    fixed (int* hexRef = toHexTable) 
    fixed (byte* sourceRef = source) 
    { 
     byte* s = sourceRef; 
     int resultLen = (source.Length << 1); 

     var result = new string(' ', resultLen); 
     fixed (char* resultRef = result) 
     { 
      int* pair = (int*)resultRef; 

      while (*pair != 0) 
       *pair++ = hexRef[*s++]; 
      return result; 
     } 
    } 
} 

Вот мои "улучшения":

public static unsafe string ToHex1p(byte[] source) 
{ 
    var chunks = Environment.ProcessorCount; 
    var n = (int)Math.Ceiling(source.Length/(double)chunks); 

    int resultLen = (source.Length << 1); 

    var result = new string(' ', resultLen); 

    Parallel.For(0, chunks, k => 
    { 
     var l = Math.Min(source.Length, (k + 1) * n); 
     fixed (char* resultRef = result) fixed (byte* sourceRef = source) 
     { 
      int from = n * k; 
      int to = (int)resultRef + (l << 2); 

      int* pair = (int*)resultRef + from; 
      byte* s = sourceRef + from; 
      while ((int)pair != to) 
       *pair++ = toHexTable[*s++]; 
     } 
    }); 

    return result; 
} 


Edit 1 Это, как я время функции:

var n = 0xff; 
var s = new System.Diagnostics.Stopwatch(); 
var d = Enumerable.Repeat<byte>(0xce, (int)Math.Pow(2, 23)).ToArray(); 

s.Start(); 
for (var i = 0; i < n; ++i) 
{ 
    Binary.ToHex1(d); 
} 
Console.WriteLine(s.ElapsedMilliseconds/(double)n); 

s.Restart(); 
for (var i = 0; i < n; ++i) 
{ 
    Binary.ToHex1p(d); 
} 
Console.WriteLine(s.ElapsedMilliseconds/(double)n); 
+0

Почему это внутри и снаружи цикла For? fixed (char * resultRef = result) fixed (byte * sourceRef = source) - см. http: // stackoverflow.com/questions/8497018/what-is-the-off-of-c-sharp-fixed-statement-on-a-managed-unsafe-struct-conta – tolanj

+0

@tolanj: Я не могу ответить за OP, но я подозревают, что это связано с тем, что в анонимном методе гораздо проще помещать 'fixed', чем извне, из-за правил о захвате указателей. Для усмешек я пошел вперед и протестировал его с помощью «фиксированного» снаружи, и обнаружил, что это вообще не имеет значения. Обратите внимание, что в этом случае утверждения 'fixed' выполняются только один раз для потока; отнимающий много времени цикл _is_ внутри операторов 'fixed'. –

+0

@PeterDuniho: Соглашайтесь с порядком оценки. В debug-vs-release ваш опыт намного отличается от моего. Это особенно актуально, когда вы сравниваете работу с приложенным и не подключенным отладчиком. Я видел совершенно разные относительные тайминги. То есть алгоритм A значительно быстрее, чем B с отладкой, и значительно медленнее, чем B при выпуске. –

ответ

4

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

Когда я переключаю порядок испытаний, параллельный один срабатывает быстрее, чем непараллельный. Это первый признак того, что тест несправедлив. :)

Когда я сменил тесты так, что после каждого теста я вызвал GC.Collect(), чтобы гарантировать, что GC был тихим во время последующего, параллельная версия последовательно выходила вперед. Но только едва ли; время запуска для каждого потока было во всех случаях более половины общей продолжительности каждого параллельного теста.

В рамках моего тестирования я модифицировал код так, чтобы он отслеживал фактическое время, проведенное в каждом потоке версии For(). Здесь я обнаружил, что время, проведенное в этом коде, было о том, чего я ожидал бы на основе непараллельной версии (т. Е. Достаточно близко к времени, деленному на количество потоков).

(Конечно, эту информацию можно получить и с помощью профилировщика).

Вот результаты тестов, которые я провел с GC.Collect(). Для параллельного теста это также показывает начало (относительно общего времени начала теста) и время продолжительности для каждого потока.

Запуск непараллельного версии первого, параллельная версия вторая:

Однозаходные версия: 00: 00: 00,6726813
Параллельная версия: 00: 00: 00,6270247
    Thread # 0: Старт: 00: 00: 00.3343985, продолжительность: 00: 00: 00,2925963
    Thread # 1: Начало: 00: 00: 00,3345640, продолжительность: 00: 00: 00,2805527

Single-Автошоу версия объявления: 00: 00: 00,7027335
Параллельная версия: 00: 00: 00,5610246
    Thread # 0: старт: 00: 00: 00.3305695, продолжительность: 00: 00: 00,2304486
    Thread # 1: начало : 00: 00: 00.3305857, продолжительность: 00:00:00.2300950

версия Однозаходные: 00: 00: 00,6609645
Параллельная версия: 00: 00: 00,6143675
    Thread # 0: старт: 00: 00: 00,3391491, продолжительность: 00: 00: 00,2750529
    Thread # 1: Начало: 00: 00: 00.3391560, продолжительность: 00: 00: 00,2705631

Однозаходные версия: 00: 00: 00,6655265
Параллельная версия: 00: 00: 00,6246624
    Thread # 0: старт: 00: 00: 00.3227595, продолжительность: 00: 00: 00,2924611
    Thread # 1: Начало: 00: 00: 00,3227831, продолжительность: 00: 00: 00,3018066

Одно- версия тема: 00: 00: 00,6815009
Параллельная версия: 00: 00: 00,5707794
    Thread # 0: старт: 00: 00: 00.3227074, продолжительность: 00: 00: 00,2480668
    Thread # 1: начало : 00: 00: 00.3227330, продолжительность: 00: 00: 00.2478351

Запуск параллельной версии первого, не параллельна второй:

Параллельная версия: 00: 00: 00,5807343
    Thread # 0: старт: 00: 00: 00,3397320, продолжительность: 00:00: 00.2409767
    Thread # 1: Начало: 00: 00: 00.3398103, продолжительность: 00: 00: 00,2408334
версия Однозаходные: 00: 00: 00,6974992

Параллельная версия: 00: 00: 00,5801044
    Thread # 0: старт: 00: 00: 00.3305571, продолжительность: 00: 00: 00,2495409
    Thread # 1: Начало: 00: 00: 00,3305746, продолжительность: 00: 00: 00,2492993
Однозаходные версия: 00: 00: 00,7442493

Параллельная версия: 00: 00: 00,5845514
    Thread # 0: старт: 00: 00: 00.3454512, продолжительность: 00: 00: 00,2352147
    Thread # 1: начало: 00: 00: 00.3454756, продолжительность: 00: 00: 00.2389522
Однопоточная версия: 00: 00: 00,6542540

Параллельная версия: 00: 00: 00,5909125
    Thread # 0: старт: 00: 00: 00.3356177, продолжительность: 00: 00: 00,2550365
    Thread # 1: старт : 00: 00: 00,3356250, продолжительность: 00: 00: 00,2552392
версия Однозаходные: 00: 00: 00,7609139

Параллельная версия: 00: 00: 00,5777678
    Thread # 0: старт: 00 : 00: 00.3440084, продолжительность: 00:00:00.2337504
    Thread # 1: Начало: 00: 00: 00.3440323, продолжительность: 00: 00: 00,2329294
версия Однозаходные: 00: 00: 00.6596119

уроки:

  • Тестирование производительности сложно, особенно в управляемой среде. Такие вещи, как сбор мусора и сборка «точно в срок», затрудняют сравнение яблок с яблоками
  • Фактические вычислительные затраты на преобразование байтов в символы абсолютно несущественны по сравнению с чем-либо, что программа может тратить свое время (например, подготовка и вызов потоков). Этот конкретный алгоритм не кажется целесообразным распараллеливать; даже несмотря на то, что вы добиваетесь постоянного улучшения скорости, это довольно незначительно из-за всех накладных расходов вокруг фактических вычислений.

Последнее замечание: еще одним источником ошибок в этих тестах является Intel Hyperthreading. Вернее, если вы проверите, используя подсчет CPU с поддержкой Hyperthread, вы получите неверные результаты. Например, я тестировал это на своем ноутбуке на базе Intel i5, который сообщает, что имеет 4 ядра. Но запуск четырех потоков не приближается к 4-кратной ускоренной работе над непараллельной реализацией, а запуск двух потоков будет близок к ускорению 2x (для правильной проблемы). Вот почему, хотя мой компьютер сообщает 4 процессора, я тестировал только 2 потока.

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

+0

Хорошая работа. Любопытно, что «GC.Collect» даже не разобрался. Было бы интересно узнать, отличаются ли ваши результаты от 'gcServer enabled = true' в app.config. –

+0

Что вы подразумеваете под «даже вещами»? Использование 'GC.Collect() 'действительно позволял параллельной версии последовательно выигрывать. Из-за непараллельных накладных расходов он просто не выигрывает. –

+0

Тогда я не понимаю ваших результатов. В первом случае (параллельно выполняется второй), он выглядит как параллельная версия в среднем 25 мс. Во втором случае (параллельно выполняется параллельный запуск), похоже, параллельная версия работает примерно через 13 мс. Что я недопонимаю? –

0

Я прочитал в комментарии к первому ответу (который, кстати, более информативен, чем вопрос и комментарии вообще), что время выполнения этих тестов составляет порядка 25 мс максимум.

Есть много вещей, чтобы сказать об этом, но первое: «Какая трата хорошего времени программиста!»

Это ясный случай, когда вы чрезмерно оптимизируете. С первого взгляда на код, я подумал: «Почему в мире кто-то хочет параллелизировать это?» Вы выполняете побитовые операции, которые очень быстрые. В первую очередь, для оправдания параллелизма недостаточно оценки эффективности.

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

Как программисты, мы должны помнить как правило, что наше время чрезвычайно дорого, особенно по сравнению с компьютерным временем. Возможности для достижения истинного прироста производительности с добавленной стоимостью редко встречаются при программировании и, как правило, вокруг очень дорогостоящей обработки или случаев, когда вам нужно запрашивать с удаленных серверов и т. Д. Есть определенные моменты, когда это необходимо, но это не одно из их.