Parallel.For с расчетами BigInteger, отличными от цикла For - PullRequest
0 голосов
/ 07 сентября 2018

У меня есть следующий цикл, который выполняет преобразование из base95 в base10. Я работаю с несколькими тысячами цифр, поэтому необходимы BigIntegers. inst является строкой base95.

Parallel.For(0, inst.Length, x => 
     {
        result += BigInteger.Pow(95, x) * (inst[x] - 32);
     });

Если я работаю со строками base95 длиной около 200 символов или менее, он работает отлично и выдает тот же результат, что и обычный цикл for.

Однако, как только я начинаю увеличивать размер строки base95, вывод параллели сбрасывается. Мне нужно работать со строками base95 с 1500+ символами и даже до 30000. Обычный цикл for может хорошо вычислить результат.

Что может быть причиной этой проблемы? Есть ли лучший способ для этого, чем цикл Parallel.For, который все еще быстрее, чем цикл for?

1 Ответ

0 голосов
/ 07 сентября 2018

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

Единственным решением будет сделать его безопасным для потоков. Дешевым и неприятным подходом будет использование lock ... Было бы лучше, если бы вы могли использовать другой потокобезопасный подход, такой как Interlocked, однако он не будет работать с BigInteger.

BigInteger result = 0;
object sync = new object();

Parallel.For(
   0,
   inst.Length,
   x =>
      {
         var temp = BigInteger.Pow(95, x) * (inst[x] - 32);
         lock (sync)
            result += temp;
      });

Он не идеален со всеми блокировками, но все же быстрее, чем обычная петля for на моем компьютере

Другим подходом было бы использование перегрузок, таким образом вы блокируете только один раз для каждого потока.

Parallel.For(
   0,
   inst.Length,
   () => new BigInteger(0),
   (x, state, subTotal) => subTotal + BigInteger.Pow(95, x) * (inst[x] - 32),
   integer =>
      {
         lock (sync)
            result += integer;
      });

Ориентиры

Так мне было скучно, вот ваши отметки

Тесты проводились по 50 раз каждый, GC.Collect и GC.WaitForPendingFinalizers запускаются перед каждым тестом, чтобы дать более четкие результаты. Все результаты были проверены друг против друга, чтобы доказать, что они точны. Scale представляет размер строки в соответствии с вашим вопросом

Настройка

----------------------------------------------------------------------------
Mode             : Release (64Bit)
Test Framework   : .NET Framework 4.7.1 (CLR 4.0.30319.42000)
----------------------------------------------------------------------------
Operating System : Microsoft Windows 10 Pro
Version          : 10.0.17134
----------------------------------------------------------------------------
CPU Name         : Intel(R) Core(TM) i7-3770K CPU @ 3.50GHz
Description      : Intel64 Family 6 Model 58 Stepping 9
Cores (Threads)  : 4 (8)      : Architecture  : x64
Clock Speed      : 3901 MHz   : Bus Speed     : 100 MHz
L2Cache          : 1 MB       : L3Cache       : 8 MB
----------------------------------------------------------------------------

Результаты

--- Random characters -----------------------------------------------------------------
| Value          |    Average |    Fastest |    Cycles | Garbage | Test |        Gain |
--- Scale 10 ----------------------------------------------------------- Time 0.259 ---
| for            |   5.442 µs |   4.968 µs |  21.794 K | 0.000 B | Base |      0.00 % |
| ParallelResult |  32.451 µs |  30.397 µs | 116.808 K | 0.000 B | Pass |   -496.25 % |
| ParallelLock   |  35.551 µs |  32.443 µs | 127.966 K | 0.000 B | Pass |   -553.22 % |
| AsParallel     | 141.457 µs | 118.959 µs | 398.676 K | 0.000 B | Pass | -2,499.13 % |
--- Scale 100 ---------------------------------------------------------- Time 0.298 ---
| ParallelResult |  93.261 µs |  80.085 µs | 329.450 K | 0.000 B | Pass |     11.36 % |
| ParallelLock   | 103.912 µs |  84.470 µs | 366.599 K | 0.000 B | Pass |      1.23 % |
| for            | 105.210 µs |  93.823 µs | 371.025 K | 0.000 B | Base |      0.00 % |
| AsParallel     | 183.538 µs | 159.002 µs | 488.534 K | 0.000 B | Pass |    -74.45 % |
--- Scale 1,000 -------------------------------------------------------- Time 4.191 ---
| AsParallel     |   5.701 ms |   4.932 ms |  15.479 M | 0.000 B | Pass |     65.83 % |
| ParallelResult |   6.510 ms |   5.701 ms |  18.166 M | 0.000 B | Pass |     60.98 % |
| ParallelLock   |   6.734 ms |   5.303 ms |  17.314 M | 0.000 B | Pass |     59.64 % |
| for            |  16.685 ms |  15.640 ms |  58.183 M | 0.000 B | Base |      0.00 % |
--- Scale 10,000 ------------------------------------------------------ Time 34.805 ---
| AsParallel     |    6.205 s |    4.767 s |  19.202 B | 0.000 B | Pass |     47.20 % |
| ParallelResult |    6.286 s |    5.891 s |  14.752 B | 0.000 B | Pass |     46.51 % |
| ParallelLock   |    6.290 s |    5.202 s |   9.982 B | 0.000 B | Pass |     46.48 % |
| for            |   11.752 s |   11.436 s |  41.136 B | 0.000 B | Base |      0.00 % |
---------------------------------------------------------------------------------------

ParallelLock

[Test("ParallelLock", "", true)]
public BigInteger Test1(string input, int scale)
{
   BigInteger result = 0;
   object sync = new object();

   Parallel.For(
      0,
      input.Length,
      x =>
         {
            var temp = BigInteger.Pow(95, x) * (input[x] - 32);
            lock (sync)
               result += temp;
         });

   return result;
}

ParallelResult

[Test("ParallelResult", "", false)]
public BigInteger Test2(string input, int scale)
{
   BigInteger result = 0;
   object sync = new object();
   Parallel.For(
      0,
      input.Length,
      () => new BigInteger(0),
      (x, state, subTotal) => subTotal + BigInteger.Pow(95, x) * (input[x] - 32),
      integer =>
         {
            lock (sync)
               result += integer;
         });
   return result;
}

AsParallel в соответствии с предложением GDIR

[Test("AsParallel", "", false)]
public BigInteger Test4(string input, int scale)
{
   return Enumerable.Range(0, input.Length)
                    .AsParallel()
                    .Aggregate(
                        new BigInteger(0),
                        (subtotal, x) => subtotal + BigInteger.Pow(95, x) * (input[x] - 32),
                        (total, thisThread) => total + thisThread,
                        (finalSum) => finalSum);;
}

для

[Test("for", "", false)]
public BigInteger Test3(string input, int scale)
{       
   BigInteger result = 0;
   for (int i = 0; i < input.Length; i++)
   {
      result += BigInteger.Pow(95, i) * (input[i] - 32);
   }
   return result;
}

Input

public static string StringOfChar(int scale)
{
   var list = Enumerable.Range(1, scale)
                        .Select(x => (char)(_rand.Next(32)+32))
                        .ToArray();
   return string.Join("", list);
} 

Валидация

private static bool Validation(BigInteger result, BigInteger baseLine)
{
   return result == baseLine;
}

Резюме

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

в любом случае, удачи

...