Здравствуйте, vdimas, Вы писали:
V>Ты спрятал важную подробность алгоритма.
V>Причём, за нечитаемым кодом.
В промышленной библиотеке эта подробность была бы документирована.
S>>Мне интересна ровно обратная сторона — записать алгоритм так, чтобы скрыть несущественные подробности.
V>ОК, так и запишем: разрабатывать эффективные хелперы для обхода двумерных массивов Синклер согласен только для Linq и ни боже упаси для чего-то "другого".
Я постараюсь объяснить в четвёртый раз: я вообще не пытаюсь разработать "эффективные хелперы". Я пытаюсь разделить "полный алгоритм" на ту часть, которая "обходит" массив, и ту часть, которая определяет ядро фильтра.
V>Нечитабельно.
Как улучшить читаемость?
V>Одна строчка описывает только часть алгоритма, а не целиком его.
Потому что именно эта часть является "переменной". А стратегия обхода будет более-менее одна для широкого класса фильтров.
V>Полностью если? Намного меньше, чем в твоём полном решении.
Это нечестное сравнение. Давайте тогда включим в оценку размера кода
from a in {1, 2, 3} where a % 2 == 0 select a весь Enumerable.cs.
Ключевая идея — в том, что код linq-провайдера можно использовать повторно.
А из вашего кода с4 сделать с8 можно только копи-пейстом и никак иначе.
Если у вас есть какая-то идея, как суметь избежать выписывания вложенных fixed для каждого конкретного ядра — велком, рассказывайте.
V>Твои — категорически нет.
А по-моему — наоборот.
V>>>"Лапша" тут может быть лишь от попыток свалить все части алгоритма в один цикл.
S>>Ну вы же именно это и предложили.
V>Я пока не предлагал ничего, кроме как сделать сравнение честным, т.е. если уж оптимизируется доступ к двумерному массиву, то пусть он оптимизируется для ВСЕХ сравниваемых техник.
Я основываюсь на тех примерах кода, которые вы написали. Вот, цитируем:
unsafe void FourNeighborAverage(T array) where T : struct, IArray2d<int>
{
int dx = array.DX;
for(int y = 1, dy = array.DY - 1; y < dy; y++) {
fixed(int* current = array[1, y])
fixed(int* end = array[dx, y])
fixed(int* it1 = array[0, y])
fixed(int* it2 = array[2, y])
fixed(int* it3 = array[1, y - 1])
fixed(int* it4 = array[1, y + 1]) {
int* c = current, _1 = it1, _2 = it2, _3 = it3, _4 = it4;
while(c < end)
*c++ = *_1++ + *_2++ + *_3++ + *_4++;
}
}
}
Здесь — всё вместе:
— описание ядра фильтра: строчки 6-9 и 12
— учёт границ (который нужно обновлять при каждой смене ядра): строчки 3,4,5
— собственно алгоритм обхода — строчки 3 и 11.
V>Но не в том же цикле.
А в каком? Вот же он код. Там нет никаких других отдельных циклов.
V>Я ХЗ как можно оставлять процитированное мною, при этом приписывая мне ровно противоположное тому, что я говорил.
Код говорит сам за себя.
V>Выглядит так, что ты опять и снова запаниковал, бо тут уже запахло сравнением реализации ПОЛНОГО алгоритма, а не упрощенного.
V>Т.е. когда требуется обработать краевые эффекты согласно некоторому параметру (через заданный цвет бесконечности или через расширение картинки за счёт крайних пикселей).
Ну, давайте расширим требования. Покажите мне, как легко изменить ваш алгоритм на случай, например, заданного цвета бесконечности.
V>Ну и где у тебя ограничение прохода лишь по безопасной области массива?
V>Где и как это выражено?
V>Ткни пальцем в участок своего кода, если я что-то упустил.
Сейчас оно захардкожено в генераторе MSIL:
ilg.EmitIncrease(h_var, -km.ymax);
ilg.EmitIncrease(w_var, -km.xmax);
И в тех местах, где EmitFor().
V>>>Ты начни туда прикручивать where, например, сделать c4 только для абсолютно чёрных точек.
S>>И в чём проблема? Вы не понимаете, как работает where clause, или что?
V>Я понимаю как работает перегрузка методов-расширений, поэтому и предлагаю, таки, окунуться чуть поближе, а потом уже делать выводы.
Ну, если вы настаиваете, то вот как выглядит решение задачи "если пиксел — чёрный, то заменить его средним от соседей, а если нет — то скопировать":
var filtered = from d in data select d[0,0] != 0 ? d[0,0]: (d[-1, 0] + d[1, 0] + d[0, -1] + d[0, 1]) / 4;
Пока что нет никаких проблем с сигнатурами. Как, впрочем, и с where, который тут не нужен.
V>А я рядом пример уже приводил.
Пример чего?
V>Просто пока у тебя в сигнатурах не появилось Select<T, TResult, TCollection>(this TCollection, ...) where TCollection : ISomeInterface<T> — никакой проблемы нет, разумеется.
А зачем мне это? Просто берём this IArray2d<T> и всё.
V>Обойти это можно только через ConcreteCollection2<TResult> Select<T, TResult>(this ConcreteCollection1<T>, ...), где ConcreteCollection1 — эдакие value-type обертки-маркеры. Но начав покрывать сценарии из обсуждаемой предметной области ты быстро обнаружишь резкий рост комбинаторики на ровном месте. В "стандартном" линке типы стираются через боксинг, но мы-то хотим его избежать, верно?
Мы избегаем боксинга другим способом, поэтому нет нужды ни в value-типах для обёртывания, ни в сложных where.
V>Без боксинга если, то вовсе не 9-ти.
V>Наивно считал, что простого напоминания о граблях будет достаточно.
"Напоминания о граблях" и прочие туманные намёки на толстые обстоятельства неинтересны.
S>>В реальной промышленной библиотеке конечно же будет возвращаться не T[,], а promise, который будет резолвиться либо явно через .ToArray() либо неявно через вызов [,].
S>>Конечно же будет строиться вся цепочка операций.
V>Ага.
V>Только у value-типов никаким образом не стираются типы.
Ну и что.
V>А до тех пор будешь носить клеймо необъективного человека.
Последний шанс даю. Ещё слово про "носить клеймо" или ещё какие характеристики личности — и я продолжу дискуссию с более вменяемыми оппонентами.
V>Сейчас уже можно без unsafe:
V>V> [StructLayout(LayoutKind.Sequential, Pack =1)]
V> public struct Rgb24 {
V> public byte R;
V> public byte G;
V> public byte B;
V> public Rgb24(byte r, byte g, byte b) {
V> R = r;
V> G = g;
V> B = b;
V> }
V> }
V> class Program {
V> static void Main(string[] args) {
V> Rgb24[,] image = {
V> { new Rgb24(1, 2, 3), new Rgb24(4, 5, 6) },
V> { new Rgb24(7, 8, 9), new Rgb24(10, 11, 12) }
V> };
V> var bytes = MemoryMarshal.AsBytes(MemoryMarshal.CreateReadOnlySpan(ref image[0, 0], image.Length));
V> foreach(byte b in bytes)
V> Console.WriteLine(b);
V> }
V> }
V>
V>Пока оно немного уступает unsafe по эффективности, но обещается, что будет то же самое.
Прекрасно. Осталось посмотреть, как будет выглядеть тот же c4 с примененем этого подхода. Я к тому, что чудес-то не бывает, и возможность реинтерпретации памяти не сделает наш код красивее.
V>В показанном тобой коде это не так.
Почему?
S>Похоже, ты в "гарантии" отнёс только защиту от выхода за границы массива, но пренебрег корректностью алгоритма.
Я брал конкретный алгоритм от alex_public. Потому что не хотел подменять задачу — если мы начнём беспокоиться о корректности, то for for вообще превратится в макароны.
V>Разница не у тебя должна была появиться, а у "идиоматического" кода.
Код с обходом массива — 1х. Код с обходом "индексера" — 0.7х. Код linq — 0.5х. Код с unsafe обходом массива или индексера — 0.49х.
.
V>2. В твоей формулировке не утверждалось, что идиоматический код не может пользоваться хелперами.
Да пусть пользуется, мне что, жалко что ли.
V>Хотя в реальных алгоритмах обработки изображений (на тех же плюсах) весь "идиматический" код (т.е. написанный в виде явного итерирования) — он всегда работает поверх неких аналогов показанных мною "индексеров". Потому что по-другому в этой предметной области никак, бо форматов/кодировок даже RAW-изображений слишком много.
С форматами и кодировками С# не очень хорошо. Я сходу не могу придумать удачного способа написать повторно используемый код усреднения для пикселов, скажем, Rgb16.
Ну, то есть понятно, что можно попробовать сделать что-то типа
public struct Rgb565 : IPixel
{
private ushort raw;
public int R
{
get => (raw & 0xF800) >> 11;
set => raw = (ushort)((raw & ~0xF800) | (value << 11) & 0xF800);
}
public int G
{
get => (raw & 0x07E0) >> 6;
set => raw = (ushort)((raw & ~0x07E0) | (value << 6) & 0x07E0);
}
public int B
{
get => (raw & 0x001F) ;
set => raw = (ushort)((raw & ~0x001F) | (value & ~0x001F));
}
}
Но есть у меня сомнения в том, что джит породит приемлемый код, даже если сделать
public P Avg<P>(P p1, P p2) where P:unmanaged, IPixel
{
return new P{
R = (p1.R+p2.R)/2
G = (p1.G+p2.G)/2
B = (p1.B+p2.B)/2
}
}
V>В показанном тобой виде возможность отсутствует как класс. Задача не решена. Итоговые пиксели подсчитаны неверно.
Строго так же, как в оригинальном решении от alex_public.
V>Т.е., я пока не увидел в твоём коде ничего помимо трюка с ускорением обхода массива.
Основной "трюк" — это инлайнинг делегата, с попутной заменой относительных обращений на абсолютные. Всё остальное — рутина, которая малоинтересна.
V>Пока же тебе удалось (впервые в жизни, судя по уровню радости) написать метод-расширение под линк и тебя банально понесло, ты забыл быть объективным.
Мне удалось написать метод-расширение для чего-то необычного. Отсюда и радость.
V>Потому что ты подаешь на вход абстрактный ref-тип, которым является массив.
V>А даже если пробовал Array2d, то максимум в одном сценарии, т.е. еще не нарвался на каскадные Select.
А мне вполне достаточно абстрактного реф-типа, например IArray2d<T>.
Потому что внутри я вообще не пользуюсь обращениями к [,].
Каскадные селекты работают тем же способом — в первом приближении.
То есть у нас получается цепочка вызовов монолитных методов с порождением промежуточных массивов.
Дальнейшее развитие может "склеивать" такие цепочки — чтобы в финале был один проход по исходному массиву с генерацией сразу окончательного результата, безо всякой промежуточности.
Вот как раз этим я сейчас и занимаюсь, на примере бинаризации по Sauvola, предложенной Павлом.
Код пока находится в раздолбанном состоянии, потому и не выкладываю.
V>Давай ты в следующий раз не будешь поднимать градус нервозности по собственной инициативе.
V>Бери пример с меня — я принципиально отвечаю в подобном ключе только в ответ.
Поднятие градуса нервозности — это вбежать в топик, и сходу, не разобравшись, начать хамить и оскорблять оппонента. Вот, например, как
здесь.
Я просто не каждый раз вам указываю на эти некорректности. В основном — потому, что остальным читателям интересны технические вопросы, а не вопросы вашего воспитания.
Так что оферта с баном остаётся открытой. sapienti sat.