Информация об изменениях

Сообщение Re[30]: 2D-Linq и оптимизация цифровых фильтров - 3 от 12.07.2018 14:52

Изменено 12.07.2018 14:53 Sinclair

Re[30]: 2D-Linq и оптимизация цифровых фильтров - 3
Здравствуйте, vdimas, Вы писали:
V>А как джит спасает от тел этих методов?
V>Для одномерного массива вот в этом случае: for(int k=0; k<array.Length; k++) sum+=array[k]; компилятор просто подставляет опкоды доступа к массиву безо всяких проверок.
Неважно, что подставляет компилятор.
Важно, какой код получается в целевой архитектуре.
В одномерном случае есть специальный опкод ldelema.
Проверка делается в нём, и джитом, а не компилятором. Достаточно посмотреть на дизассеблер запущенной программы.
Точно так же всё устроено и для многомерных массивов — вызов метода get_Item заменяется на серию проверок по количеству измерений.

V>Осталось понять, как это противоречит замеченной мною разнице в эффективности м/у одномерными и двумерными массивами и моему утверждению, что ты свёл доступ к двумерному массиву к доступу к одномерному, т.е. сыграл на этой разнице. ))

Разнице в эффективности — не противоречит. Утверждению — противоречит. Доступ к одномерному массиву тоже подлежит проверке. Я избавился от проверок вообще. То есть в "честном" коде мы имеем 2 проверки на обращение, в "индексере" — одну, в unsafe коде — 0.

V>А, главное, почему это можно делать для linq-хелпера и нельзя делать для "обычных циклов"?

Потому что для "обычных" циклов этот корявый код пиннинга и адресной арифметики придётся писать вручную, рискуя накосячить. А для linq код порождает тупая неумолимая железяка, которая не делает ошибок.

S>>А если мы перелезаем через забор и пользуемся fixed в bulk-операциях, то разницы нет, т.к. сравнивать будем 0 проверок и 0 проверок.

V>А самим не надо в любом случае делать проверки выхода за диапазон в ценарии "относительной адресации"? ))
Нет, не надо. Я же показал код — там нет никаких проверок, и при этом всё безопасно.

V>Cамая издевательская тут последняя строка z — внутри Span<> всё-равно происходят проверки выхода за диапазон, хотя на строке x мы вышли за диапазон массива без проблем.

V>Тоже налицо улыбающие попытки совместить ужа с ежом.
Налицо непонимание идеи, стоящей за Span<T>.
Она, вкратце, вот в чём:
1. Обеспечить возможность писать алгоритмы, которые универсально применимы к данным в хипе, на стеке, и в неуправляемой памяти.
2. Обеспечить высокую эффективность.
В частности, вот в таком цикле проверки на выход за границы делаются 0 раз:
public void PrintArray(int[] a)
{
  for(int i=0; i<a.Length; i++) Console.WriteLine(a);
}

Вот в таком — ~Length раз:
public void PrintArray(int[] a)
{
  for(int i=0; i<a.Length -1; i++) Console.WriteLine(a);
}

А в таком — 1 раз:
public void PrintArray(int[] a)
{
  var s = new Span<int>(a, a.Length-1); // вот здесь делается единственная проверка
  for(int i=0; i<s.Length; i++) Console.WriteLine(a);
}

Для перехода между вторым и третьим случаями нам и полезен Span<T>.

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


V>Когда весь алгоритм у нас перед глазами, т.е. мы заведомо "обслужили единички", которые ты предлагал "вручную не обслуживать" (С), то мы можем не делать проверок на каждой итерации, бо у нас прямо перед глазами находится причина, разрешающая нам избегать проверок.

Мы — можем. Джит — нет. Он недостаточно умён, чтобы заметить, что мы учли размер ядра в диапазонах обхода.
Отказаться от проверок в дотнете можно только одним способом — адресной арифметикой.
Лепить адресную арифметику в каждом конкретном цикле — боже упаси. А если мы захотим туда припилить SIMD интринсики и/или многопоточность, то там вообще будет туши свет.
Такие вещи перемешивать с прикладным кодом строго противопоказано.

V>Получил при попытке обобщения противоречивые инварианты для того самого базового решения.

Пока что никаких противоречий я не вижу. По крайней мере таких, которые было бы нельзя разрешить несложным способом, при этом не жертвуя возможностью разделять ответственность.
Есть некоторый риск того, что будет раздуваться сам код генерации MSIL при увеличении количества случаев, которые надо покрыть.
Но об этом говорить пока рано — я тот код вообще ещё не причёсывал, кроме самых примитивных попыток упростить генерацию типичных конструкций.
Задача была не в том, чтобы сделать код MSIL-генератора идеальным, понятным, и расширяемым. Задача была вообще проверить, можно ли заинлайнить делегат принудительно; можно ли извлечь информацию о размере ядра программно, без участия прикладного программиста; и можно ли устранить проверки выхода за границы, оставив код безопасным.

Пока что в качестве черновика годится.

V>"Совсем красиво" задачи такого класса решаются только в языках, где можно доказать корректность обращений к агрегату.

V>В остальных же языках необходимо придерживаться наработанных индустрией правил хорошего тона.
V>Решил отвергнуть за раз целую россыпь таких наработок? — не жалуйся, критика справедлива.

V>Тем более, что в итоге и обобщённого решения не получилось, и частного тоже.

V>Подай в свой алгоритм матрицу {{2, 2}, {2, 2}} и убедись, что с4 блур обсчитывается не правильно. ))
Он обсчитывается ровно настолько же правильно, как и для матрицы 3x3. А что будет с матрицей в вашем алгоритме?
Re[30]: 2D-Linq и оптимизация цифровых фильтров - 3
Здравствуйте, vdimas, Вы писали:
V>А как джит спасает от тел этих методов?
V>Для одномерного массива вот в этом случае: for(int k=0; k<array.Length; k++) sum+=array[k]; компилятор просто подставляет опкоды доступа к массиву безо всяких проверок.
Неважно, что подставляет компилятор.
Важно, какой код получается в целевой архитектуре.
В одномерном случае есть специальный опкод ldelema.
Проверка делается в нём, и джитом, а не компилятором. Достаточно посмотреть на дизассеблер запущенной программы.
Точно так же всё устроено и для многомерных массивов — вызов метода get_Item заменяется на серию проверок по количеству измерений.

V>Осталось понять, как это противоречит замеченной мною разнице в эффективности м/у одномерными и двумерными массивами и моему утверждению, что ты свёл доступ к двумерному массиву к доступу к одномерному, т.е. сыграл на этой разнице. ))

Разнице в эффективности — не противоречит. Утверждению — противоречит. Доступ к одномерному массиву тоже подлежит проверке. Я избавился от проверок вообще. То есть в "честном" коде мы имеем 2 проверки на обращение, в "индексере" — одну, в unsafe коде — 0.

V>А, главное, почему это можно делать для linq-хелпера и нельзя делать для "обычных циклов"?

Потому что для "обычных" циклов этот корявый код пиннинга и адресной арифметики придётся писать вручную, рискуя накосячить. А для linq код порождает тупая неумолимая железяка, которая не делает ошибок.

S>>А если мы перелезаем через забор и пользуемся fixed в bulk-операциях, то разницы нет, т.к. сравнивать будем 0 проверок и 0 проверок.

V>А самим не надо в любом случае делать проверки выхода за диапазон в ценарии "относительной адресации"? ))
Нет, не надо. Я же показал код — там нет никаких проверок, и при этом всё безопасно.

V>Cамая издевательская тут последняя строка z — внутри Span<> всё-равно происходят проверки выхода за диапазон, хотя на строке x мы вышли за диапазон массива без проблем.

V>Тоже налицо улыбающие попытки совместить ужа с ежом.
Налицо непонимание идеи, стоящей за Span<T>.
Она, вкратце, вот в чём:
1. Обеспечить возможность писать алгоритмы, которые универсально применимы к данным в хипе, на стеке, и в неуправляемой памяти.
2. Обеспечить высокую эффективность.
В частности, вот в таком цикле проверки на выход за границы делаются 0 раз:
public void PrintArray(int[] a)
{
  for(int i=0; i<a.Length; i++) Console.WriteLine(a[i]);
}

Вот в таком — ~Length раз:
public void PrintArray(int[] a)
{
  for(int i=0; i<a.Length -1; i++) Console.WriteLine(a[i]);
}

А в таком — 1 раз:
public void PrintArray(int[] a)
{
  var s = new Span<int>(a, a.Length-1); // вот здесь делается единственная проверка
  for(int i=0; i<s.Length; i++) Console.WriteLine(s[i]);
}

Для перехода между вторым и третьим случаями нам и полезен Span<T>.

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


V>Когда весь алгоритм у нас перед глазами, т.е. мы заведомо "обслужили единички", которые ты предлагал "вручную не обслуживать" (С), то мы можем не делать проверок на каждой итерации, бо у нас прямо перед глазами находится причина, разрешающая нам избегать проверок.

Мы — можем. Джит — нет. Он недостаточно умён, чтобы заметить, что мы учли размер ядра в диапазонах обхода.
Отказаться от проверок в дотнете можно только одним способом — адресной арифметикой.
Лепить адресную арифметику в каждом конкретном цикле — боже упаси. А если мы захотим туда припилить SIMD интринсики и/или многопоточность, то там вообще будет туши свет.
Такие вещи перемешивать с прикладным кодом строго противопоказано.

V>Получил при попытке обобщения противоречивые инварианты для того самого базового решения.

Пока что никаких противоречий я не вижу. По крайней мере таких, которые было бы нельзя разрешить несложным способом, при этом не жертвуя возможностью разделять ответственность.
Есть некоторый риск того, что будет раздуваться сам код генерации MSIL при увеличении количества случаев, которые надо покрыть.
Но об этом говорить пока рано — я тот код вообще ещё не причёсывал, кроме самых примитивных попыток упростить генерацию типичных конструкций.
Задача была не в том, чтобы сделать код MSIL-генератора идеальным, понятным, и расширяемым. Задача была вообще проверить, можно ли заинлайнить делегат принудительно; можно ли извлечь информацию о размере ядра программно, без участия прикладного программиста; и можно ли устранить проверки выхода за границы, оставив код безопасным.

Пока что в качестве черновика годится.

V>"Совсем красиво" задачи такого класса решаются только в языках, где можно доказать корректность обращений к агрегату.

V>В остальных же языках необходимо придерживаться наработанных индустрией правил хорошего тона.
V>Решил отвергнуть за раз целую россыпь таких наработок? — не жалуйся, критика справедлива.

V>Тем более, что в итоге и обобщённого решения не получилось, и частного тоже.

V>Подай в свой алгоритм матрицу {{2, 2}, {2, 2}} и убедись, что с4 блур обсчитывается не правильно. ))
Он обсчитывается ровно настолько же правильно, как и для матрицы 3x3. А что будет с матрицей в вашем алгоритме?