Код, о котором идет речь:
int s = 32343;
ArrayList<Integer> al = new ArrayList<Integer>(s);
for (int c = 0; c < s; c++) {
al.add(c);
}
Iterator<Integer> it = al.iterator();
if (it.hasNext()) {
int c = it.next();
c = c++;
}
Конечно, это снизит производительность, если вы увеличите значение s
, так как s
контролирует, сколько вещей вы поместите в список. Но это очень мало связано с параллелизмом или масштабируемостью. Если вы пишете код, говорящий компьютеру тратить время на тысячи или миллионы одноразовых вычислений, то, конечно, ваша производительность будет ухудшаться.
В более технических терминах сложность времени этого раздела кода составляет O(2n)
(для построения списка требуется n
операций, а затем для выполнения итераций n
операций и увеличения каждого значения), где n
равно s
. Таким образом, чем больше вы сделаете s
, тем дольше будет выполняться этот код.
С точки зрения того, почему это может уменьшить преимущества параллелизма, рассматривали ли вы последствия для памяти, когда s
становится больше? Например, вы уверены, что куча Java достаточно велика, чтобы хранить все в памяти, не выгружая ее на диск? И даже если ничего не выгружается, увеличивая длину ArrayList
, вы даете сборщику мусора больше работы во время его работы (и, возможно, увеличиваете частоту, с которой он работает). Обратите внимание, что в зависимости от реализации сборщик мусора может приостанавливать все ваши потоки при каждом запуске.
Интересно, если вы выделите один экземпляр ArrayList
для каждого потока во время создания потока, а затем повторно используете это при вызове isPrime()
вместо того, чтобы каждый раз создавать новый список, это улучшает ситуацию?
Редактировать: Вот исправленная версия: http://pastebin.com/6vR7Uhez
Это дает следующий вывод на моей машине:
------------------start------------------
1 threads' runtimes:
1 3766.0
maximum: 3766.0
main time: 3766.0
------------------end------------------
------------------start------------------
2 threads' runtimes:
1 897.0
2 2483.0
maximum: 2483.0
main time: 2483.0
------------------end------------------
------------------start------------------
4 threads' runtimes:
1 576.0
2 1473.0
3 568.0
4 1569.0
maximum: 1569.0
main time: 1569.0
------------------end------------------
------------------start------------------
8 threads' runtimes:
1 389.0
2 965.0
3 396.0
4 956.0
5 398.0
6 976.0
7 386.0
8 933.0
maximum: 976.0
main time: 978.0
------------------end------------------
... который показывает почти линейное масштабирование при увеличении количества потоков. Проблемы, которые я исправил, были комбинацией пунктов, поднятых выше и в ответе Джона Винта (теперь удаленном), а также неправильное / ненужное использование структур ConcurrentLinkedQueue
и некоторая сомнительная логика синхронизации.
Если мы включим ведение журнала GC и профилируем обе версии, мы увидим, что оригинальная версия тратит в 10 раз больше времени на сборку мусора, чем модифицированная версия:
Original: [ParNew: 17401K->750K(19136K), 0.0040010 secs] 38915K->22264K(172188K), 0.0040227 secs]
Modified: [ParNew: 17024K->0K(19136K), 0.0002879 secs] 28180K->11156K(83008K), 0.0003094 secs]
Что означает для меня, что между постоянными распределениями списков и Integer
автоматической коробкой исходная реализация просто перетаскивала слишком много объектов, что накладывает слишком большую нагрузку на ГХ, что снижает производительность ваших потоков до такой степени, что не было никакой выгоды (или даже негативной выгоды) для создания большего количества потоков.
Итак, все это говорит мне о том, что если вы хотите добиться хорошего масштабирования параллелизма в Java, независимо от того, является ли ваша задача большой или маленькой, вы должны обратить внимание на то, как вы используете память, осознавать потенциально скрытые ловушки и неэффективность, и оптимизировать неэффективные биты.