Почему этот алгоритм в Ruby работает быстрее, чем в Parallel'd С#?
Следующий код ruby работает в ~ 15 секунд. Он почти не использует процессор/память (около 25% от одного процессора):
def collatz(num)
num.even? ? num/2 : 3*num + 1
end
start_time = Time.now
max_chain_count = 0
max_starter_num = 0
(1..1000000).each do |i|
count = 0
current = i
current = collatz(current) and count += 1 until (current == 1)
max_chain_count = count and max_starter_num = i if (count > max_chain_count)
end
puts "Max starter num: #{max_starter_num} -> chain of #{max_chain_count} elements. Found in: #{Time.now - start_time}s"
И следующий TPL С# помещает все мои 4 ядра в 100% использование и на несколько порядков медленнее, чем рубиновая версия:
static void Euler14Test()
{
Stopwatch sw = new Stopwatch();
sw.Start();
int max_chain_count = 0;
int max_starter_num = 0;
object locker = new object();
Parallel.For(1, 1000000, i =>
{
int count = 0;
int current = i;
while (current != 1)
{
current = collatz(current);
count++;
}
if (count > max_chain_count)
{
lock (locker)
{
max_chain_count = count;
max_starter_num = i;
}
}
if (i % 1000 == 0)
Console.WriteLine(i);
});
sw.Stop();
Console.WriteLine("Max starter i: {0} -> chain of {1} elements. Found in: {2}s", max_starter_num, max_chain_count, sw.Elapsed.ToString());
}
static int collatz(int num)
{
return num % 2 == 0 ? num / 2 : 3 * num + 1;
}
Почему ruby работает быстрее, чем С#? Мне сказали, что Рубин медленный. Это не так, если речь идет о алгоритмах?
Perf ПОСЛЕ коррекции:
- Ruby (Non parallel): 14.62s
- С# (непараллельно): 2.22s
- С# (с TPL): 0.64s
Ответы
Ответ 1
Собственно, ошибка довольно тонкая и не имеет ничего общего с потоками. Причина, по которой ваша версия С# занимает настолько много времени, состоит в том, что промежуточные значения, вычисленные методом collatz
, в конечном итоге начинают переполнять тип int
, что приводит к отрицательным числам, которые могут затем уходить в возрасте, чтобы сходиться.
Это происходит, когда i
составляет 134 379, для которого термин 129 th (при условии подсчета на основе одного) равен 2,482,111,348. Это превышает максимальное значение 2,147,483,647 и поэтому хранится как -1,812,855,948.
Чтобы получить хорошую производительность (и правильные результаты) в версии С#, просто измените:
int current = i;
... в:
long current = i;
... и:
static int collatz(int num)
... в:
static long collatz(long num)
Это снизит вашу производительность до респектабельных 1,5 секунд.
Изменить: CodesInChaos поднимает очень правильную точку в вопросе включения проверки переполнения при отладке приложений, ориентированных на математику. Это позволило бы сразу определить ошибку, поскольку среда выполнения создала бы OverflowException
.
![Overflow checking]()
![OverflowException]()
Ответ 2
Должно быть:
Parallel.For(1L, 1000000L, i =>
{
В противном случае у вас есть полный переполнения и начните проверку отрицательных значений. Тот же метод collatz
должен работать с длинными значениями.
Ответ 3
Я испытал нечто подобное. И я понял, что, поскольку каждая из ваших итераций цикла должна запускать другой поток, и это занимает некоторое время, и в этом случае это сопоставимо (я думаю, это больше времени), чем операции, которые вы делаете в теле цикла.
Есть альтернатива для этого: вы можете получить, сколько у вас ядер процессора, и чем использовать цикл parallelism с тем же количеством итераций, которые у вас есть ядра, каждый цикл будет оценивать часть требуемого цикла, он сделанный путем создания внутреннего цикла цикла, который зависит от параллельного цикла.
РЕДАКТИРОВАТЬ: ПРИМЕР
int start = 1, end = 1000000;
Parallel.For(0, N_CORES, n =>
{
int s = start + (end - start) * n / N_CORES;
int e = n == N_CORES - 1 ? end : start + (end - start) * (n + 1) / N_CORES;
for (int i = s; i < e; i++)
{
// Your code
}
});
Вы должны попробовать этот код, я уверен, что это сделает работу быстрее.
EDIT: ELUCIDATION
Ну, довольно давно я ответил на этот вопрос, но снова столкнулся с проблемой и, наконец, понял, что происходит.
Я использую реализацию AForge цикла Parallel for, и кажется, что он запускает поток для каждой итерации цикла, поэтому, если цикл занимает относительно небольшое количество времени для выполнения, вы заканчиваете с неэффективным parallelism.
Итак, как некоторые из вас указали, методы System.Threading.Tasks.Parallel основаны на Tasks, которые являются более высоким уровнем абстракции потока:
"За кулисами задачи поставлены в очередь на ThreadPool, которые были расширены с помощью алгоритмов, которые определяют и корректируют количество потоков и обеспечивают балансировку нагрузки для максимальной пропускной способности. Это делает задачи относительно легкими, и вы можете создавать множество из них для обеспечения мелкозернистого parallelism.
Итак, если вы используете библиотечную реализацию по умолчанию, вам не нужно будет использовать этот "фиктивный".