Многомерные массивы 2
Sep. 28th, 2012 12:19 am![[personal profile]](https://www.dreamwidth.org/img/silk/identity/user.png)
Я тут проводил какие-то тесты над многомерными массивами дотнета, результаты которых меня несколько удивили.
Во-первых, было непонятно, что такое многомерный массив в дотнете. Он моментально выделяется и имеет относительно большое время доступа к элементам. Хотя в сравнении с моей реализацией на коленке трехмерного массива, он просто летает. Относительно Flat3d-поделки предположили, что дело в дорогих арифметических операциях и вызовах методов, которые не инлайнятся. Убрал лишний метод и немного оптимизировал арифметику. В итоге
Было (http://pastebin.com/mwqnm2re):
Стало (http://pastebin.com/2QVsRuyP):
Скорость выросла в 2 с лишним раза, а значит компилятор не поинлайнил и JIT ничегошеньки не оптимизировал.
Во-вторых, почему доступ к элементам многомерного массива в два раза медленней по сравнению с ехал массив через массив? Напоминаю:
Читал Рихтера. Он пишет, что CLR умеет нормально работать только с одномерными массивами, начальный индекс которых равен нулю. То есть нумерация элементов может начинаться и с какого-то другого значения. Как-то так:
Тут я создал по порядку: одномерный массив из 10 элементов с индексацией с нуля, потом аналогичный массив, индексируемый с единицы, а потом рассмотрел эти варианты для двухмерного случая. Ситуация очень простая: если тип массива отличен от T[] - будет медленно. Рихтер объясняет это так:
Выходит, что многомерный массив медленней из-за проверки выхода за границы на каждом такте. Это уже интересней. Представил на минутку, что у нас на дворе не 2012 год, а средневековье времен сильной инквизиции, и попробовал написать заполнение все того же многомерного массива, но с ансейфом и указателями:
Скорость работы с многомерным массивом выросла в 2 раза. А что будет с моим Flat3dArray в асейф режиме?
Абсолютно идентичный многомерному результат. По крайней мере, я теперь точно знаю, как представлен в памяти многомерный массив, но вот почему он так быстро выделяется, все равно не понимаю.
Тестовый код: http://pastebin.com/2QVsRuyP
Я еще пробовал увеличить скоростью выделения памяти под jagged array при помощи stackalloc, но в результате получался указатель, с которым можно работать только адресной арифметикой и никак иначе. Минутка кончилась, мы больше не в средневековье. А в современным мире, наверное, за такое наказывают. Решение с unsafe, я думаю, не является приемлемым. Во-первых, оно не будет работать на сильверлайте, хотя казалось бы, где еще как не на телефонах гоняться за производительностью? Во-вторых, это зайчатки ада. Все эти "я смогу аккуратно обращаться с памятью" уже проходили - дошли не все. Скорость доступа jagged array сопоставима с unsafe кодом, а скорость аллокации ситуацию не меняет, потому что не вижу задач, где надо часто выделять массивы. А, в-третьих, работу через указатели поддерживают массивы лишь органиченного числа типов, то есть решение совсем не общее.
Интересно, конечно, было поиграться, только вот осталось какое-то двоякое ощущение. С одной стороны, хорошо иметь возможно поднять производительность приложения на ровном месте, а с другой - это очевидный случай протекающей абстракции. Я даже думаю, что о подобных возможностях лучше не знать, иначе появляется соблазн почитерить вместо улучшения характеристик самого алгоритма. В худшем случае, производительность надо достигать распараллеливанием и развертыванием дополнительных машинок. Не думаю, что старое доброе дрочево "а кто же периодически срет в память" выйдет дешевле. Хотя, конечно, я-то со стороны своих задач сужу, в других областях оно, может, как-то по иному.
Во-первых, было непонятно, что такое многомерный массив в дотнете. Он моментально выделяется и имеет относительно большое время доступа к элементам. Хотя в сравнении с моей реализацией на коленке трехмерного массива, он просто летает. Относительно Flat3d-поделки предположили, что дело в дорогих арифметических операциях и вызовах методов, которые не инлайнятся. Убрал лишний метод и немного оптимизировал арифметику. В итоге
Было (http://pastebin.com/mwqnm2re):
Test fill flat array in xyz order: 26059ms (20 repeats, avr 1302ms)
Стало (http://pastebin.com/2QVsRuyP):
Test fill flat array in xyz order: 10392ms (20 repeats, avr 519ms)
Скорость выросла в 2 с лишним раза, а значит компилятор не поинлайнил и JIT ничегошеньки не оптимизировал.
Во-вторых, почему доступ к элементам многомерного массива в два раза медленней по сравнению с ехал массив через массив? Напоминаю:
Test fill jagged array in xyz order: 11,9506836 seconds (repeated 20 avr 0,59753418 seconds)
Test fill multidimension in xyz order: 22,6692966 seconds (repeated 20 avr 1,13346483 seconds)
Читал Рихтера. Он пишет, что CLR умеет нормально работать только с одномерными массивами, начальный индекс которых равен нулю. То есть нумерация элементов может начинаться и с какого-то другого значения. Как-то так:
- > Array.CreateInstance(typedefof<Int32>, [| 10 |], [| 0 |]).GetType().Name;;
- val it : string = "Int32[]"
- > Array.CreateInstance(typedefof<Int32>, [| 10 |], [| 1 |]).GetType().Name;;
- val it : string = "Int32[*]"
- > Array.CreateInstance(typedefof<Int32>, [| 10; 10 |], [| 0; 0 |]).GetType().Name;;
- val it : string = "Int32[,]"
- > Array.CreateInstance(typedefof<Int32>, [| 10; 10 |], [| 1; 1 |]).GetType().Name;;
- val it : string = "Int32[,]"
Тут я создал по порядку: одномерный массив из 10 элементов с индексацией с нуля, потом аналогичный массив, индексируемый с единицы, а потом рассмотрел эти варианты для двухмерного случая. Ситуация очень простая: если тип массива отличен от T[] - будет медленно. Рихтер объясняет это так:
Доступ к элементам одномерного массива с нулевой нижней границей осуществляется немного быстрее, чем доступ к элементам многомерных массивов или массивов с ненулевой нижней границей. Есть несколько причин такому поведению. Во-первых, специальные команды для работы с одномерными массивами с нулевой нижней границей (newarr, ldelem, ldelema , ldlen и stelem) позволяют JIТ-компилятору генерировать оптимизированный код. При этом предполагается, что первый индекс равен нулю, то есть при доступе к элементам отсутствует необходимость вычислять смещение. Кроме того, в общем случае компилятор умеет выносить код проверки границ за пределы цикла.
Выходит, что многомерный массив медленней из-за проверки выхода за границы на каждом такте. Это уже интересней. Представил на минутку, что у нас на дворе не 2012 год, а средневековье времен сильной инквизиции, и попробовал написать заполнение все того же многомерного массива, но с ансейфом и указателями:
Test fill multidimension in xyz order: 8188ms (20 repeats, avr 409ms)
Test unsafe fill multidimension in xyz order: 3898ms (20 repeats, avr 194ms)
Скорость работы с многомерным массивом выросла в 2 раза. А что будет с моим Flat3dArray в асейф режиме?
Test unssafe fill flat array in xyz order: 3850ms (20 repeats, avr 192ms
Абсолютно идентичный многомерному результат. По крайней мере, я теперь точно знаю, как представлен в памяти многомерный массив, но вот почему он так быстро выделяется, все равно не понимаю.
Тестовый код: http://pastebin.com/2QVsRuyP
Test allocate jagged array: 370ms
Test fill jagged array in xyz order: 5199ms (20 repeats, avr 259ms)
Test allocate multidimension array: 0ms
Test fill multidimension in xyz order: 8173ms (20 repeats, avr 408ms)
Test unsafe fill multidimension in xyz order: 3899ms (20 repeats, avr 194ms)
Test allocate flat array: 23ms
Test fill flat array in xyz order: 10239ms (20 repeats, avr 511ms)
Test unssafe fill flat array in xyz order: 3852ms (20 repeats, avr 192ms)
Я еще пробовал увеличить скоростью выделения памяти под jagged array при помощи stackalloc, но в результате получался указатель, с которым можно работать только адресной арифметикой и никак иначе. Минутка кончилась, мы больше не в средневековье. А в современным мире, наверное, за такое наказывают. Решение с unsafe, я думаю, не является приемлемым. Во-первых, оно не будет работать на сильверлайте, хотя казалось бы, где еще как не на телефонах гоняться за производительностью? Во-вторых, это зайчатки ада. Все эти "я смогу аккуратно обращаться с памятью" уже проходили - дошли не все. Скорость доступа jagged array сопоставима с unsafe кодом, а скорость аллокации ситуацию не меняет, потому что не вижу задач, где надо часто выделять массивы. А, в-третьих, работу через указатели поддерживают массивы лишь органиченного числа типов, то есть решение совсем не общее.
Интересно, конечно, было поиграться, только вот осталось какое-то двоякое ощущение. С одной стороны, хорошо иметь возможно поднять производительность приложения на ровном месте, а с другой - это очевидный случай протекающей абстракции. Я даже думаю, что о подобных возможностях лучше не знать, иначе появляется соблазн почитерить вместо улучшения характеристик самого алгоритма. В худшем случае, производительность надо достигать распараллеливанием и развертыванием дополнительных машинок. Не думаю, что старое доброе дрочево "а кто же периодически срет в память" выйдет дешевле. Хотя, конечно, я-то со стороны своих задач сужу, в других областях оно, может, как-то по иному.
(no subject)
Date: 2012-09-27 09:10 pm (UTC)(no subject)
Date: 2012-09-27 09:19 pm (UTC)Test allocate jagged array: 992ms
Test fill jagged array in xyz order: 13522ms (20 repeats, avr 676ms)
Test allocate multidimension array: 0ms
Test fill multidimension in xyz order: 19429ms (20 repeats, avr 971ms)
Test unsafe fill multidimension in xyz order: 8168ms (20 repeats, avr 408ms)
Test allocate flat array: 106ms
Test fill flat array in xyz order: 26256ms (20 repeats, avr 1312ms)
Test unssafe fill flat array in xyz order: 8124ms (20 repeats, avr 406ms)
(no subject)
Date: 2012-09-27 09:13 pm (UTC)2. если у вас GC, то не влияет ли порядок аллокации на время аллокации (на этапе, когда нужно выделить плоский массив, у вас память всё ещё занята мультиdimensional массивом)
(no subject)
Date: 2012-09-27 09:35 pm (UTC)2) Похоже, что влияет. Стал прогонять тесты Flat3DArray первыми.
Test allocate flat array: 1ms
Test allocate multidimension array: 2ms
Это многое объясняет, но многомерный выделяется быстро вне зависимости от обстоятельств. Если тесты Flat3DArray запускать не самыми первыми, то его время аллокации возвращается на привычные 100+ms.
(no subject)
Date: 2012-09-28 07:22 am (UTC)Расскажу, как в джаве.
JIT может и соптимизировать, но постесняется заместить метод на оптимальный, пока вызов не вернулся из метода.
Количество вызовов метода влияет на количество и типы оптимизаций. (инлайн полиморфного кода - когда есть статистика, какие инстансы появляются чаще всего; глубина инлайна зависит от количества вызовов; loop unrolling зависит от количества времени внутри цикла; condition reversion зависит от наиболее вероятного результата; dead code elimination = пурый код с неиспользуемым результатом удаляется)
Allocation у вас всего по одному разу. Сделать побольше хип и погонять несколько раз в разной последовательности.
Измерить не только среднее, но и stdev. Прогретая машина даст маленький stdev.
(no subject)
Date: 2012-09-27 10:58 pm (UTC)Test allocate jagged array: 540ms
Test fill jagged array in xyz order: 3225ms (20 repeats, avr 161ms)
Test allocate multidimension array: 798ms
Test fill multidimension in xyz order: 10327ms (20 repeats, avr 516ms)
Test unsafe fill multidimension in xyz order: 1485ms (20 repeats, avr 74ms)
Test allocate flat array: 1066ms
Test fill flat array in xyz order: 6741ms (20 repeats, avr 337ms)
Test unssafe fill flat array in xyz order: 1454ms (20 repeats, avr 72ms)
потом я попросил его запустить бинарник собранный студийным компилятором сишарпа:
Test allocate jagged array: 518ms
Test fill jagged array in xyz order: 3232ms (20 repeats, avr 161ms)
Test allocate multidimension array: 278ms
Test fill multidimension in xyz order: 9945ms (20 repeats, avr 497ms)
Test unsafe fill multidimension in xyz order: 1486ms (20 repeats, avr 74ms)
Test allocate flat array: 292ms
Test fill flat array in xyz order: 6463ms (20 repeats, avr 323ms)
Test unssafe fill flat array in xyz order: 1446ms (20 repeats, avr 72ms)
Вот под моно, никаких неувязок нет. Под виндой, видимо, есть какие-то хаки, про которые писал thedeemon.
(no subject)
Date: 2012-09-28 07:27 am (UTC)Test fill flat array in xyz order: 6741ms
Test fill multidimension in xyz order: 9945ms
Test fill flat array in xyz order: 6463ms"
это улучшенная арифметика, так? Значит, ещё разница на проверке внутренних размерностей, которые у multidimensional есть, а у flat нет.
(no subject)
Date: 2012-09-28 09:05 am (UTC)(no subject)
Date: 2012-09-28 04:34 am (UTC)Ну а в целом картина совершенно ожидаемая. Оптимизаций в C# и CLR немного (быстро компиляет, быстро запускает, думать некогда вообще), bounds checks убираются только в самом примитивном случае, шаг в сторону - и они остаются. За все это приходится платить тактами.
(no subject)
Date: 2012-09-28 09:02 am (UTC)Я не разбираюсь в оптимизациях, ни разу у меня не было, чтоб флаги оптимизации давали прирост производительности, о котором стоило бы говорить. Всегда приходилось менять структуры данных и алгоритмы, если возникали проблемы со скоростью. Наверное, мне надо почитать, а что там в теории дотнет оптимизирует.
Что касается тактов, то мне скинули варианты этой задачи на фортране и си, так они в 2 раза проигрывают худшему из вариантов на дотнете.
(no subject)
Date: 2012-09-28 10:26 am (UTC)А вот это уже удивительно. Как и чем собирались?
(no subject)
Date: 2012-09-28 10:56 am (UTC)Сишный код (сам код смогу дать лишь вечером, поскольку он остался на нетбуке) я сначала собрал плюсовым gcc со всеми оптимизациями, потом компилятором c++ из студии. Время заполенения превысилос 100 и 130 скунд соответственно. Ад.
Фортрановый код (http://pastebin.com/1C2x6pwd) был собран вот этим поделием (http://www.silverfrost.com/11/ftn95_overview.aspx), поскольку мне было лениво разбираться с gcc фортраном под cygwin'ом.
Думаю, и там и там есть ошибки.
(no subject)
Date: 2012-09-28 08:17 pm (UTC)allocate: 0.00ms memset: 329.00ms fill: 120040.00ms average: 6002.00ms
http://pastebin.com/0EmGCkGp
(no subject)
Date: 2012-09-29 03:55 am (UTC)(no subject)
Date: 2012-09-29 05:42 am (UTC)Оригинал:
allocate: 0.00ms memset: 78.00ms fill: 25069.00ms average: 1253.45ms
Теперь делаем его ближе к варианту на С#. Во-первых, массив там инициализируется нулем, поэтому вместо явного заполнения его говядиной просто заменяем malloc на calloc:
allocate & memset: 0.00ms fill: 25568.00ms average: 1278.40ms
Во-вторых, приводим в соответствие тело главного цикла:
array[128*(512*x + y) + z] = i++;
Получаем:
allocate & memset: 0.00ms fill: 1014.00ms average: 50.70ms
Разогнали в 25 раз.
(no subject)
Date: 2012-09-29 10:49 am (UTC)(no subject)
Date: 2012-09-29 11:20 am (UTC)(no subject)
Date: 2012-09-29 05:48 am (UTC)Test allocate jagged array: 314ms
Test fill jagged array in xyz order: 2864ms (20 repeats, avr 143ms)
Test allocate multidimension array: 0ms
Test fill multidimension in xyz order: 3788ms (20 repeats, avr 189ms)
Test unsafe fill multidimension in xyz order: 1064ms (20 repeats, avr 53ms)
Test allocate flat array: 14ms
Test fill flat array in xyz order: 1877ms (20 repeats, avr 93ms)
Test unssafe fill flat array in xyz order: 1119ms (20 repeats, avr 55ms)
Теперь картина ожидаемая - unsafe чуть медленнее С++, safe сильно медленнее.
(no subject)
Date: 2012-09-29 10:43 am (UTC)>Test fill flat array in xyz order: 1877ms (20 repeats, avr 93ms)
> safe сильно медленнее
Не так уж сильно, разницы на порядки нет. Может посмотрю, как ведут другие яп высокого уровня, позиционируемые как производительные: D, OCaml, ObjC. Пока результаты дотнета мне представляются весьма неплохими.
(no subject)
Date: 2012-09-29 11:26 am (UTC)В окамле массивы сравнительно медленные, а в 32-битной версии еще и заметно ограниченные по длине: jagged сделать получится, а для flat придется брать bigarray.
(no subject)
Date: 2012-09-28 05:12 am (UTC)(no subject)
Date: 2012-09-28 05:40 am (UTC)(no subject)
Date: 2012-09-28 09:06 am (UTC)(no subject)
Date: 2012-09-28 09:17 am (UTC)