C# to Native C++
От: barn_czn  
Дата: 09.09.09 06:52
Оценка:
Доброго дня всем.
Кто нибудь знает, как сделать сабж?
Нагуглил вот это : http://www.tangiblesoftwaresolutions.com/?gclid=CNeQqPjz45wCFVWF3godKjU3Gw
но демка так ничего и не сконвертила.

Корни проблемы растут вот откуда. Есть у меня своя либа на C#, в которой реализованы алгоритмы обработки изображения.
Алгоритмы юзают OpenCV (т.е. сделаны .NET обертки над CvMat, CvArr и другими типами OpenCV).
В целом получается очень даже удобно экспериментировать с алгоритмами, юзать любые фичи OpenCV. Намного проще чем колбасить код
на С/С++, особенно если учесть что опыта на С практически нет.
Перфоманс на стадии экспериментов меня вполне устраивает.

Начал дальше работать над перфомансом и вот проблема: unsafe код, работающий напрямую с указателями (double*) работает все равно медленее чем тот же код (один в один) написаный и скомпиленный в нативной dll (даже с затратами на интероп). Не понимаю почему, ведь чисто unsafe код, без использования .NET массивов и других классов..

Остается один выход — узкие места переносить в нативный С++ код. А это ведет к копипасту (полному или частичному) классов из C# кода в С++.
Отсюда желание как то это автоматизировать. Ведь если в коде нет использования чисто .NET-овских классов — то это вообшще должно быть просто.

Кто что думает? Переходить полностью на С++ у меня нет ни желания ни времени.
Re: C# to Native C++
От: nikov США http://www.linkedin.com/in/nikov
Дата: 09.09.09 07:11
Оценка:
Здравствуйте, barn_czn, Вы писали:

_>Кто что думает? Переходить полностью на С++ у меня нет ни желания ни времени.


Ты уже посмотрел профайлером, где именно самое медленное место в коде?
Re[2]: C# to Native C++
От: barn_czn  
Дата: 09.09.09 07:35
Оценка:
Здравствуйте, nikov, Вы писали:

N>Здравствуйте, barn_czn, Вы писали:


_>>Кто что думает? Переходить полностью на С++ у меня нет ни желания ни времени.


N>Ты уже посмотрел профайлером, где именно самое медленное место в коде?


Конечно, dotTrace юзал.

Тормоза например на поэлементной обработке матриц (в цикле проходим по строкам и постолбцам).
Сначала делал это также как пишут в доках к OpenCv:

a_ij = ((double*)(mat.data.Ptr + mat.step * i)[j]
— медленно, главная проблема — приведение к (double*)

потом сделал для каждой матрицы предварительное формирование массива указателей строк (т.е. массив double*[])
— стало быстрее.
Перейти на double** думаю не сильно ускорит.

В общем проверено, unsafe код на .NET не компилится в эффективный нативный код (в райнтайме имею ввиду когда IL->машинный код).
Re[3]: C# to Native C++
От: Пельмешко Россия blog
Дата: 09.09.09 08:43
Оценка:
Здравствуйте, barn_czn, Вы писали:

_>a_ij = ((double*)(mat.data.Ptr + mat.step * i)[j]

_>- медленно, главная проблема — приведение к (double*)

а какого типа (mat.data.Ptr + mat.step * i)?
Re[4]: C# to Native C++
От: barn_czn  
Дата: 09.09.09 09:24
Оценка:
Здравствуйте, Пельмешко, Вы писали:

П>Здравствуйте, 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;
}
}
— это способ работы с матрицой, самый быстрый который я нашел.
Re[3]: C# to Native C++
От: Sinclair Россия https://github.com/evilguest/
Дата: 10.09.09 06:28
Оценка: 9 (1) +2 :)))
Здравствуйте, barn_czn, Вы писали:
_>Тормоза например на поэлементной обработке матриц (в цикле проходим по строкам и постолбцам).
_>Сначала делал это также как пишут в доках к OpenCv:

_>a_ij = ((double*)(mat.data.Ptr + mat.step * i)[j]

_>- медленно, главная проблема — приведение к (double*)
Непонятно, почему вы не хотите использовать арифметику указателей.
Примерно так:
double* ptr = (double*)mat.data.Ptr;
for(int i = 0; i<rowCount; i++)
  for(int j = 0; j<colCount; j++)
  {
    a_ij = *ptr;
    ptr++; // 
  }

Здесь нет приведений к *double внутри цикла.
... << RSDN@Home 1.2.0 alpha rev. 677>>
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Re: C# to Native C++
От: Pavel Dvorkin Россия  
Дата: 10.09.09 06:49
Оценка:
Здравствуйте, barn_czn, Вы писали:


_>Начал дальше работать над перфомансом и вот проблема: unsafe код, работающий напрямую с указателями (double*) работает все равно медленее чем тот же код (один в один) написаный и скомпиленный в нативной dll (даже с затратами на интероп). Не понимаю почему, ведь чисто unsafe код, без использования .NET массивов и других классов..


Оптимизация, однако. Если вместо VC++ взять Intel C++ — может, еще немного выиграешь.
With best regards
Pavel Dvorkin
Re[4]: C# to Native C++
От: barn_czn  
Дата: 10.09.09 10:43
Оценка:
Здравствуйте, Sinclair, Вы писали:

S>Здравствуйте, barn_czn, Вы писали:

_>>Тормоза например на поэлементной обработке матриц (в цикле проходим по строкам и постолбцам).
_>>Сначала делал это также как пишут в доках к OpenCv:

_>>a_ij = ((double*)(mat.data.Ptr + mat.step * i)[j]

_>>- медленно, главная проблема — приведение к (double*)
S>Непонятно, почему вы не хотите использовать арифметику указателей.
S>Примерно так:
S>
S>double* ptr = (double*)mat.data.Ptr;
S>for(int i = 0; i<rowCount; i++)
S>  for(int j = 0; j<colCount; j++)
S>  {
S>    a_ij = *ptr;
S>    ptr++; // 
S>  }
S>

S>Здесь нет приведений к *double внутри цикла.

Объясняю. Строки в матрицах, в битмапах, практически во всех либах не следуют друг за другом.
Они следуют с некоторым постоянным смещением.

a_ij = ((double*)(mat.data.Ptr + mat.step * i)[j]

— здесь mat.step не я придумал, так Intel задумал, и очень правильно сделал.

Какой в этом смысл? Во первых выравнивание в памяти. Поправте меня если я не прав но кажется это как то влияет на перфоманс.
Во вторых это возможность из матриц (битмапов) извлекать подматрицы (думаю сами поймете как).

Очень жаль что в .NET не сделали такой возможности с массивами: например неплохо было бы даже из одномерных массивов извлекать подмассив без копирования в другой. Но это я уже не по теме.
Re[5]: C# to Native C++
От: Sinclair Россия https://github.com/evilguest/
Дата: 10.09.09 11:00
Оценка:
Здравствуйте, 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>>
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Re[6]: C# to Native C++
От: barn_czn  
Дата: 11.09.09 09:42
Оценка:
Здравствуйте, 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) ?
Думаю бага будет.
Re[7]: C# to Native C++
От: Sinclair Россия https://github.com/evilguest/
Дата: 11.09.09 10:17
Оценка:
Здравствуйте, barn_czn, Вы писали:
_>Целочисленное деление?? И что будет если mat.Step не делится на цело на sizeof(double) ?
_>Думаю бага будет.
Кто-то только что рассказывал про выравнивание, или мне показалось?

Если mat.step не делится нацело на 8 (то есть выравнивание таки сломано), то можно и принудительно инкрементировать void* поинтер. Всё равно "приведений к double*" будет на порядки меньше.
Вы, вместо того, чтобы обсуждать очевидные мелочи, лучше запустите эту модификацию под профайлером, и расскажите, что получилось. Есть мнение, что не в приведении типов там дело.
... << RSDN@Home 1.2.0 alpha rev. 677>>
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Re[8]: C# to Native C++
От: barn_czn  
Дата: 12.09.09 02:32
Оценка:
S>Кто-то только что рассказывал про выравнивание, или мне показалось?

S>Если mat.step не делится нацело на 8 (то есть выравнивание таки сломано), то можно и принудительно инкрементировать void* поинтер. Всё равно "приведений к double*" будет на порядки меньше.

S>Вы, вместо того, чтобы обсуждать очевидные мелочи, лучше запустите эту модификацию под профайлером, и расскажите, что получилось. Есть мнение, что не в приведении типов там дело.

Я не против обсуждать очевидные мелочи если бы это хоть как то помогло делу.
Способ с делением степа на размер типа я нигде не видел чтобы юзали, поэтому сомневаюсь что это правильно. В доках OpenCV есть вполне конкретный ответ на то как обращатся к элементам матрицы, там умножение с приведением, и я им почему то больше верю. Вообще, какая разница, приведение не приведение, если бы .NET нормальный код генерил — это бы работало также быстро (или также медленно) как на С++. Однако нет, любой алгоритм реализованый на С++ все равно работает быстрее — вот в чем суть вопроса этого топика, а не в том как с матрицами работать.
Ну конечно дело не в приведении типов, дело в .NET, как я сразу и сказал, и поэтому топик называется C# to C++.
Re[8]: C# to Native C++
От: barn_czn  
Дата: 12.09.09 02:53
Оценка: :)
На счет деления степа вы действительно правы, в исходниках OpenCV используется такой прием..
Но не думаю что это даст какое то преимущество C# перед C++.
Re[2]: C# to Native C++
От: Аноним  
Дата: 12.09.09 06:22
Оценка:
Ну что, защитники дотнета, кто говорил, что C# как бы рвет C++ во всех тестах? (Здесь еще используется unsafe-код, заметьте.)

(Это ни разу не троллинг, просто хотелось бы услышать от Nikov'а объяснение, в чем тут может быть дело.)
Re[3]: C# to Native C++
От: Мизантроп  
Дата: 12.09.09 08:40
Оценка: +3
Здравствуйте, Аноним, Вы писали:

А>Ну что, защитники дотнета, кто говорил, что 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?

Но автор проверять это не хочет. Похоже, он уже принял решение на эмоциональном уровне, а в таком случае попытки переубедить с помощью логики зачастую бесперспективны.
"Нормальные герои всегда идут в обход!"
Re[4]: C# to Native C++
От: barn_czn  
Дата: 12.09.09 15:12
Оценка:
М>Но автор проверять это не хочет. Похоже, он уже принял решение на эмоциональном уровне, а в таком случае попытки переубедить с помощью логики зачастую бесперспективны.

Уверяю вас, что у меня нет никакого желания писать на С++, но мне приходится выносить узкие места в нативный С++.
На счет проверки. В коде на шарпе я избавился от приведения с помощью деления степа на 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 скажет.)
Re[5]: C# to Native C++
От: Мизантроп  
Дата: 12.09.09 16:44
Оценка:
Здравствуйте, 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. А получилось вот что:
fld         qword ptr [ebp] 
fmul        qword ptr [ebx] 
fstp        qword ptr [esp+8]

На случай, если понимание аcсемблера представляет для Вас затруднение, я поясню. Вся операция умножения выполняется 3-ия командами процессора. Первая загружает в арифметический сопроцессор 8-мибайтное число, адрес которого находится в регистре EBP процессора. Вторая приказывает сопроцессору перемножить ранее загруженное число с числом, адрес которого находится в регистре EBX. Наконец третья команда приказывает сопроцессору выгрузить полученный результат в локальную переменную, расположенную, естественно, на стеке. Адрес этой переменной на 8 больше числа, лежащего в регистре ESP.

Если Вы скомпилируете подобный код в каком-нибудь нативном языке, и посмотрите полученный ассемблерный листинг, то увидите те-же самые три команды. Могут отличаться имена регистров или величина смещения, но сами команды будут те-же самые, просто потому, что более быстрого способы выполнить это действие на данном процессоре не существует. А как Вы, надеюсь, понимаете, три одинаковые команды процессор выполнит с одинаковой скоростью, на каком бы языке ни был написан породивший их высокоуровневый код.

Я не хотел-бы показаться навязчивым, но может быть стоит пока воздержаться от подведения итогов? Может быть Вы ещё что-нибудь пока не знаете, не только про выравнивание, как Вы думаете?
"Нормальные герои всегда идут в обход!"
Re[5]: C# to Native C++
От: Мизантроп  
Дата: 12.09.09 17:12
Оценка:
Здравствуйте, Аноним, Вы писали:

М>>Может быть здесь дело в попытке перенести в NET "as is" подходы, хорошо работающие в нативном коде, без учёта особенностей NET?


А>Да дадно. Случай, описанный здесь — далеко не первый, я о таком слышу уже в N-ый раз.


А>Каждый раз одна и та же история: обычный программист, не являющийся искушенным экспертом (а таких большинство), пишет идентичный (насколько это возможно) код на C++ и на C#, и на C++ работает быстрее.


Если человек не знает C++, то он и на нём напишет такое, что "мама, не горюй". И даже данный случай это лемонстрирует — смотрите высказывание о том, что после переделки код на C# стал работать быстрее первоначального кода на Си Так что разговоры про "неискушённость" — это и есть отмазка. Программист обязан знать инструмент, которым пользуется, это аксиома. Мне приходилось видеть слишком много абсолютно безграмотного с точки зрения оптимизации, с совершенно очевидными тормозами кода, написанного на самых разных языках. Но одна вещь эти языки объединяла — все они не имели ни малейшего отношения к NET. Безграмотность никак не коррелирует ни с используемым языком, ни с платформой.

Но все эти слова, как видно, ни к чему. Два моих предыдущих поста одназначно показывают, что код, полученный после JIT-компиляции, абсолютно такой-же, какой сгенерил бы любой нативный компилятор, но Вы благополучно этот факт проигнорировали. Похоже, Вы пришли сюда с единственной целью — поругаться с конкретным человеком, а факты Вас не интересуют. Что-же, дело Ваше.
"Нормальные герои всегда идут в обход!"
Re[6]: C# to Native C++
От: Аноним  
Дата: 12.09.09 19:43
Оценка:
М>Откомпилируем и посмотрим, во что-же вылилась команда умножения двух адресуемых по ссылке чисел double. А получилось вот что:
М>
М>fld         qword ptr [ebp] 
М>fmul        qword ptr [ebx] 
М>fstp        qword ptr [esp+8]
М>


М>Если Вы скомпилируете подобный код в каком-нибудь нативном языке, и посмотрите полученный ассемблерный листинг, то увидите те-же самые три команды. Могут отличаться имена регистров или величина смещения, но сами команды будут те-же самые, просто потому, что более быстрого способы выполнить это действие на данном процессоре не существует.


Как бы фигвам. Ключевое слово SSE ни о чем не говорит? И потом, вытаскивание трех команд из кода для оценки скорости их выполнения — занятие бессмысленное, а на современной архитектуре Intel — бессмысленное в кубе.

Кстати, интересно, что JIT-комплиятор обломался и не использовал SSE.
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.