Корни проблемы растут вот откуда. Есть у меня своя либа на C#, в которой реализованы алгоритмы обработки изображения.
Алгоритмы юзают OpenCV (т.е. сделаны .NET обертки над CvMat, CvArr и другими типами OpenCV).
В целом получается очень даже удобно экспериментировать с алгоритмами, юзать любые фичи OpenCV. Намного проще чем колбасить код
на С/С++, особенно если учесть что опыта на С практически нет.
Перфоманс на стадии экспериментов меня вполне устраивает.
Начал дальше работать над перфомансом и вот проблема: unsafe код, работающий напрямую с указателями (double*) работает все равно медленее чем тот же код (один в один) написаный и скомпиленный в нативной dll (даже с затратами на интероп). Не понимаю почему, ведь чисто unsafe код, без использования .NET массивов и других классов..
Остается один выход — узкие места переносить в нативный С++ код. А это ведет к копипасту (полному или частичному) классов из C# кода в С++.
Отсюда желание как то это автоматизировать. Ведь если в коде нет использования чисто .NET-овских классов — то это вообшще должно быть просто.
Кто что думает? Переходить полностью на С++ у меня нет ни желания ни времени.
Здравствуйте, nikov, Вы писали:
N>Здравствуйте, barn_czn, Вы писали:
_>>Кто что думает? Переходить полностью на С++ у меня нет ни желания ни времени.
N>Ты уже посмотрел профайлером, где именно самое медленное место в коде?
Конечно, dotTrace юзал.
Тормоза например на поэлементной обработке матриц (в цикле проходим по строкам и постолбцам).
Сначала делал это также как пишут в доках к OpenCv:
a_ij = ((double*)(mat.data.Ptr + mat.step * i)[j]
— медленно, главная проблема — приведение к (double*)
потом сделал для каждой матрицы предварительное формирование массива указателей строк (т.е. массив double*[])
— стало быстрее.
Перейти на double** думаю не сильно ускорит.
В общем проверено, unsafe код на .NET не компилится в эффективный нативный код (в райнтайме имею ввиду когда IL->машинный код).
Здравствуйте, Пельмешко, Вы писали:
П>Здравствуйте, barn_czn, Вы писали:
_>>a_ij = ((double*)(mat.data.Ptr + mat.step * i)[j] _>>- медленно, главная проблема — приведение к (double*)
П>а какого типа (mat.data.Ptr + mat.step * i)?
public double*[] DoubleRows
{
get
{
if (_doubleRows == null)
{
_doubleRows = new double*[_matHeader.rows];
for (int i = 0; i < _matHeader.rows; i++)
{
_doubleRows[i] = (double*)((int)_matHeader.data + _matHeader.step * i);
}
}
return _doubleRows;
}
}
— это способ работы с матрицой, самый быстрый который я нашел.
Здравствуйте, barn_czn, Вы писали: _>Тормоза например на поэлементной обработке матриц (в цикле проходим по строкам и постолбцам). _>Сначала делал это также как пишут в доках к OpenCv:
_>a_ij = ((double*)(mat.data.Ptr + mat.step * i)[j] _>- медленно, главная проблема — приведение к (double*)
Непонятно, почему вы не хотите использовать арифметику указателей.
Примерно так:
_>Начал дальше работать над перфомансом и вот проблема: unsafe код, работающий напрямую с указателями (double*) работает все равно медленее чем тот же код (один в один) написаный и скомпиленный в нативной dll (даже с затратами на интероп). Не понимаю почему, ведь чисто unsafe код, без использования .NET массивов и других классов..
Оптимизация, однако. Если вместо VC++ взять Intel C++ — может, еще немного выиграешь.
Здравствуйте, Sinclair, Вы писали:
S>Здравствуйте, barn_czn, Вы писали: _>>Тормоза например на поэлементной обработке матриц (в цикле проходим по строкам и постолбцам). _>>Сначала делал это также как пишут в доках к OpenCv:
_>>a_ij = ((double*)(mat.data.Ptr + mat.step * i)[j] _>>- медленно, главная проблема — приведение к (double*) S>Непонятно, почему вы не хотите использовать арифметику указателей. S>Примерно так: S>
Объясняю. Строки в матрицах, в битмапах, практически во всех либах не следуют друг за другом.
Они следуют с некоторым постоянным смещением.
a_ij = ((double*)(mat.data.Ptr + mat.step * i)[j]
— здесь mat.step не я придумал, так Intel задумал, и очень правильно сделал.
Какой в этом смысл? Во первых выравнивание в памяти. Поправте меня если я не прав но кажется это как то влияет на перфоманс.
Во вторых это возможность из матриц (битмапов) извлекать подматрицы (думаю сами поймете как).
Очень жаль что в .NET не сделали такой возможности с массивами: например неплохо было бы даже из одномерных массивов извлекать подмассив без копирования в другой. Но это я уже не по теме.
Здравствуйте, barn_czn, Вы писали:
_>Объясняю. Строки в матрицах, в битмапах, практически во всех либах не следуют друг за другом. _>Они следуют с некоторым постоянным смещением.
_>a_ij = ((double*)(mat.data.Ptr + mat.step * i)[j] _>- здесь mat.step не я придумал, так Intel задумал, и очень правильно сделал.
Прекрасно. Немножко перепишем код:
double* rowStart = (double*)mat.data.Ptr;
for(int i = 0; i<rowCount; i++)
{
double* ptr = rowStart;
for(int j = 0; j<colCount; j++)
{
a_ij = *ptr;
ptr++; //
}
rowStart += mat.Step/sizeof(double); // вы почему-то избегаете приводить объявление mat и его мемберов. приходится угадывать.
}
По-прежнему никаких приведений. Что говорит профайлер?
... << RSDN@Home 1.2.0 alpha rev. 677>>
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Здравствуйте, Sinclair, Вы писали:
S>Здравствуйте, barn_czn, Вы писали:
_>>Объясняю. Строки в матрицах, в битмапах, практически во всех либах не следуют друг за другом. _>>Они следуют с некоторым постоянным смещением.
_>>a_ij = ((double*)(mat.data.Ptr + mat.step * i)[j] _>>- здесь mat.step не я придумал, так Intel задумал, и очень правильно сделал. S>Прекрасно. Немножко перепишем код: S>
S>double* rowStart = (double*)mat.data.Ptr;
S>for(int i = 0; i<rowCount; i++)
S>{
S> double* ptr = rowStart;
S> for(int j = 0; j<colCount; j++)
S> {
S> a_ij = *ptr;
S> ptr++; //
S> }
S> rowStart += mat.Step/sizeof(double); // вы почему-то избегаете приводить объявление mat и его мемберов. приходится угадывать.
S>}
S>
S>По-прежнему никаких приведений. Что говорит профайлер?
Целочисленное деление?? И что будет если mat.Step не делится на цело на sizeof(double) ?
Думаю бага будет.
Здравствуйте, barn_czn, Вы писали: _>Целочисленное деление?? И что будет если mat.Step не делится на цело на sizeof(double) ? _>Думаю бага будет.
Кто-то только что рассказывал про выравнивание, или мне показалось?
Если mat.step не делится нацело на 8 (то есть выравнивание таки сломано), то можно и принудительно инкрементировать void* поинтер. Всё равно "приведений к double*" будет на порядки меньше.
Вы, вместо того, чтобы обсуждать очевидные мелочи, лучше запустите эту модификацию под профайлером, и расскажите, что получилось. Есть мнение, что не в приведении типов там дело.
... << RSDN@Home 1.2.0 alpha rev. 677>>
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
S>Кто-то только что рассказывал про выравнивание, или мне показалось?
S>Если mat.step не делится нацело на 8 (то есть выравнивание таки сломано), то можно и принудительно инкрементировать void* поинтер. Всё равно "приведений к double*" будет на порядки меньше. S>Вы, вместо того, чтобы обсуждать очевидные мелочи, лучше запустите эту модификацию под профайлером, и расскажите, что получилось. Есть мнение, что не в приведении типов там дело.
Я не против обсуждать очевидные мелочи если бы это хоть как то помогло делу.
Способ с делением степа на размер типа я нигде не видел чтобы юзали, поэтому сомневаюсь что это правильно. В доках OpenCV есть вполне конкретный ответ на то как обращатся к элементам матрицы, там умножение с приведением, и я им почему то больше верю. Вообще, какая разница, приведение не приведение, если бы .NET нормальный код генерил — это бы работало также быстро (или также медленно) как на С++. Однако нет, любой алгоритм реализованый на С++ все равно работает быстрее — вот в чем суть вопроса этого топика, а не в том как с матрицами работать.
Ну конечно дело не в приведении типов, дело в .NET, как я сразу и сказал, и поэтому топик называется C# to C++.
На счет деления степа вы действительно правы, в исходниках OpenCV используется такой прием..
Но не думаю что это даст какое то преимущество C# перед C++.
Re[2]: C# to Native C++
От:
Аноним
Дата:
12.09.09 06:22
Оценка:
Ну что, защитники дотнета, кто говорил, что C# как бы рвет C++ во всех тестах? (Здесь еще используется unsafe-код, заметьте.)
(Это ни разу не троллинг, просто хотелось бы услышать от Nikov'а объяснение, в чем тут может быть дело.)
Здравствуйте, Аноним, Вы писали:
А>Ну что, защитники дотнета, кто говорил, что C# как бы рвет C++ во всех тестах? (Здесь еще используется unsafe-код, заметьте.)
Вряд-ли меня можно отнести к упомянутой Вами категории, но позволю себе всё-же высказаться.
Я вот тоже думал, что код в NET априори работает медленне нативного. Однако-же мои первые, достаточно неуклюжие пробы шарпа дают несколько иную картину. Есть конечно ситуации, где заметно явное отставание от натива, хотя и по большей части не катастрофичное. Но во многих случаях код, написанный на шарпе, если и отстаёт, то это отставание вполне укладывается в погрешность измерений. Превосходства, правда, я пока тоже не видел, но ведь и опыта в NET у меня — с Гулькин нос
А>(Это ни разу не троллинг, просто хотелось бы услышать от Nikov'а объяснение, в чем тут может быть дело.)
Я тоже надеюсь, что уважаемый Nikov выскажет своё мнение на этот счёт, а пока выскажу свою гипотезу. Может быть здесь дело в попытке перенести в NET "as is" подходы, хорошо работающие в нативном коде, без учёта особенностей NET? Вот предлагает-же Sinclair самоочевидную, на мой взгляд, вещь — коль скоро некая операция явно тормозит, то очевидно-же, что следует минимизировать количество таких операций.
С другой стороны, я вот сейчас специально посмотрел — привидение Int32 к указателю на double скомпилилось в одну-единственную ассемблерную команду:
mov ebp,edi
Обе переменные были локальными. Для Int64 получилось
dword ptr [esp+8],esi
а для глобальной Int64:
mov eax,dword ptr ds:[00B68920h]
mov ebx,eax
То есть совершенно то-же самое, что сделал бы и нативный компилятор. Так может быть дело всё-таки не в NET?
Но автор проверять это не хочет. Похоже, он уже принял решение на эмоциональном уровне, а в таком случае попытки переубедить с помощью логики зачастую бесперспективны.
М>Но автор проверять это не хочет. Похоже, он уже принял решение на эмоциональном уровне, а в таком случае попытки переубедить с помощью логики зачастую бесперспективны.
Уверяю вас, что у меня нет никакого желания писать на С++, но мне приходится выносить узкие места в нативный С++.
На счет проверки. В коде на шарпе я избавился от приведения с помощью деления степа на sizeof(double), спасибо Sinclair за наводку на как казалось сначало пустую мысль. К стыду своему я не понимал смысла выравнивания строк, я думал это выравнивание на 1-2 байта специально под физические особенности процессоров. Так вот, после того как я избавился приведения IntPtr к double* — код на шарпе стал работать БЫСТРЕЕ чем версия на С++ (но там с приведением было).
Это хорошо, но все еще не говорит о том что .NET быстр, так как после того как сделал тоже самое в С++ коде, последний опять стал работать в 3 раза быстрее. Теперь профилировщик показывает горячие места не на обращениях к матрице а на умножениях, что в общем то нормально.
Итог. Эффективность .NET по прежнему сомнительна, но для подтверждения этого надо делать более прозрачные тесты, вероятно где то они уже есть.
Re[4]: C# to Native C++
От:
Аноним
Дата:
12.09.09 16:27
Оценка:
М>Может быть здесь дело в попытке перенести в NET "as is" подходы, хорошо работающие в нативном коде, без учёта особенностей NET?
Да дадно. Случай, описанный здесь — далеко не первый, я о таком слышу уже в N-ый раз.
Каждый раз одна и та же история: обычный программист, не являющийся искушенным экспертом (а таких большинство), пишет идентичный (насколько это возможно) код на C++ и на C#, и на C++ работает быстрее.
Конечно, большинство таких примеров написано наивно (без учета особенностей конкретной платформы), и продвинутые программисты могут устроить соревнование оптимизации под обе платформы, но нюанс как раз в том и состоит, что речь идет о простой программе, написанной простым программистом, хотя считается, что в C++ нужно затрачивать больше усилий на разработку. Получается, если вам надо удовлетворить требования производительности, программирование на .Net более трудозатратно.
Кстати, отмазка с «учетом особенностей», то есть хардкорной оптимизацией под особенности платформы, не прокатывает, так как очевидно, что в C++ в этом плане возможностей побольше будет. Если начать глубоко оптимизировать «наивные» программы, например, ввести управление памятью как в промышленных программах — с помощью специализированных алокаторов и алокаторов без блокировок, более специализированные контейнеры, а не какой-нибудь универсальный map, то очевидно, что в C++ возможностей маленько побольше будет. К тому же, с шаблонами на C++ в плане оптимизации можно гораздо шире развернуться, чем с генериками, благо еще C++0x в этом плане подсобил. Я уже не говорю про вкусности компилятора, такого как Intel Compiler типа поддержки всех последних технологий в процессорах, Global и Profile-Guided Optimization. У… куда там вашему дотнету, что вы.
Кстати, еще раз, получается, что для глубоко оптимизированных программ трудозатраты на C++ могут быть гораздо меньше, чем на .Net.
(Еще раз говорю, я не тролль, так что спорить дальше не буду, это так было — лирическое отступление. Вообще, я жду, что Nikov скажет.)
Здравствуйте, barn_czn, Вы писали:
_>Это хорошо, но все еще не говорит о том что .NET быстр, так как после того как сделал тоже самое в С++ коде, последний опять стал работать в 3 раза быстрее. Теперь профилировщик показывает горячие места не на обращениях к матрице а на умножениях, что в общем то нормально.
_>Итог. Эффективность .NET по прежнему сомнительна, но для подтверждения этого надо делать более прозрачные тесты, вероятно где то они уже есть.
Хм, Вы опять говорите о тормозах при привидении, хотя в моём предыдущем посте со всей очевидностью показано, что никаких тормозов при этом не может быть в принципе. Может стоит всё-таки над этим фактом задуматься? Может Вы что=то не то или не так, как надо, приводили?
Теперь у Вас якобы тормозит умножение. Хорошо, давайте посмотрим на умножение. Наберём такой код:
double[] d = new double[10];
for (int n = 0; n < d.Length; d[n++] = n*10) ;
fixed (double* pd = &d[0])
{
double * a1 = pd;
double * a2 = a1;
a2++;
double a3 = *a1 * *a2;
Console.WriteLine(a3);
}
Откомпилируем и посмотрим, во что-же вылилась команда умножения двух адресуемых по ссылке чисел double. А получилось вот что:
На случай, если понимание аcсемблера представляет для Вас затруднение, я поясню. Вся операция умножения выполняется 3-ия командами процессора. Первая загружает в арифметический сопроцессор 8-мибайтное число, адрес которого находится в регистре EBP процессора. Вторая приказывает сопроцессору перемножить ранее загруженное число с числом, адрес которого находится в регистре EBX. Наконец третья команда приказывает сопроцессору выгрузить полученный результат в локальную переменную, расположенную, естественно, на стеке. Адрес этой переменной на 8 больше числа, лежащего в регистре ESP.
Если Вы скомпилируете подобный код в каком-нибудь нативном языке, и посмотрите полученный ассемблерный листинг, то увидите те-же самые три команды. Могут отличаться имена регистров или величина смещения, но сами команды будут те-же самые, просто потому, что более быстрого способы выполнить это действие на данном процессоре не существует. А как Вы, надеюсь, понимаете, три одинаковые команды процессор выполнит с одинаковой скоростью, на каком бы языке ни был написан породивший их высокоуровневый код.
Я не хотел-бы показаться навязчивым, но может быть стоит пока воздержаться от подведения итогов? Может быть Вы ещё что-нибудь пока не знаете, не только про выравнивание, как Вы думаете?
Здравствуйте, Аноним, Вы писали:
М>>Может быть здесь дело в попытке перенести в NET "as is" подходы, хорошо работающие в нативном коде, без учёта особенностей NET?
А>Да дадно. Случай, описанный здесь — далеко не первый, я о таком слышу уже в N-ый раз.
А>Каждый раз одна и та же история: обычный программист, не являющийся искушенным экспертом (а таких большинство), пишет идентичный (насколько это возможно) код на C++ и на C#, и на C++ работает быстрее.
Если человек не знает C++, то он и на нём напишет такое, что "мама, не горюй". И даже данный случай это лемонстрирует — смотрите высказывание о том, что после переделки код на C# стал работать быстрее первоначального кода на Си Так что разговоры про "неискушённость" — это и есть отмазка. Программист обязан знать инструмент, которым пользуется, это аксиома. Мне приходилось видеть слишком много абсолютно безграмотного с точки зрения оптимизации, с совершенно очевидными тормозами кода, написанного на самых разных языках. Но одна вещь эти языки объединяла — все они не имели ни малейшего отношения к NET. Безграмотность никак не коррелирует ни с используемым языком, ни с платформой.
Но все эти слова, как видно, ни к чему. Два моих предыдущих поста одназначно показывают, что код, полученный после JIT-компиляции, абсолютно такой-же, какой сгенерил бы любой нативный компилятор, но Вы благополучно этот факт проигнорировали. Похоже, Вы пришли сюда с единственной целью — поругаться с конкретным человеком, а факты Вас не интересуют. Что-же, дело Ваше.
"Нормальные герои всегда идут в обход!"
Re[6]: C# to Native C++
От:
Аноним
Дата:
12.09.09 19:43
Оценка:
М>Откомпилируем и посмотрим, во что-же вылилась команда умножения двух адресуемых по ссылке чисел double. А получилось вот что: М>
М>Если Вы скомпилируете подобный код в каком-нибудь нативном языке, и посмотрите полученный ассемблерный листинг, то увидите те-же самые три команды. Могут отличаться имена регистров или величина смещения, но сами команды будут те-же самые, просто потому, что более быстрого способы выполнить это действие на данном процессоре не существует.
Как бы фигвам. Ключевое слово SSE ни о чем не говорит? И потом, вытаскивание трех команд из кода для оценки скорости их выполнения — занятие бессмысленное, а на современной архитектуре Intel — бессмысленное в кубе.
Кстати, интересно, что JIT-комплиятор обломался и не использовал SSE.