Ускорить математический код на С#, написав C dll?

У меня очень большой вложенный цикл, в котором некоторые числа и дополнения выполняются с номерами с плавающей запятой.

for (int i = 0; i < length1; i++)
{
    double aa = 0;
    for(int h = 0; h < 10; h++)
    {
       aa += omega[i][outsideGeneratedAddress[h]];
    }

    double alphaOld = alpha;
    alpha = Math.Sqrt(alpha * alpha + aa * aa);

    s = -aa / alpha;
    c = alphaOld / alpha;

    for(int j = 0; j <= i; j++)
    {
        double oldU = u[j];
        u[j] = c * oldU + s * omega[i][j];
        omega[i][j] = c * omega[i][j] - s * oldU;
    }
}

Этот цикл занимает большую часть времени обработки и является узким местом.

Возможно, я увижу какие-либо улучшения скорости, если переписать этот цикл в C и связать его с С#?

EDIT: Я обновил код, чтобы показать, как сгенерированы команды s и c. Также внутренний цикл фактически идет от 0 до i, хотя, вероятно, это не имеет большого значения для вопроса

EDIT2: Я реализовал алгоритм в VС++ и связал его с С# через dll и увидел увеличение скорости на 28% по сравнению с С# при включении всех оптимизаций. Аргумент для включения SSE2 работает особенно хорошо. Компиляция с MinGW и gcc4.4 давала только 15% -ное повышение скорости. Просто попробовал компилятор Intel и увидел повышение скорости на 49% для этого кода.

Ответы

Ответ 1

В то время как большинство других ответов, как правило, указывают на то, что вы смотрите на решения С#, большинство пропускает точку: код C для этого метода будет быстрее, если вы используете хороший оптимизирующий компилятор (я бы предложил Intel, отлично работает для этого вид кода).
Компилятор также сохранит немного работы из JIT и даст намного лучший скомпилированный вывод (даже компилятор MSVC может генерировать инструкции SSE2). Оценки массива по умолчанию не будут проверяться, вероятно, будет развертка цикла и - в целом - вы, вероятно, увидите значительное повышение производительности.
Как было правильно указано, вызов в собственный код может иметь немного накладных расходов; это должно, однако, быть незначительным по сравнению с ускорением, если длина 1 достаточно велика.
Вы можете сохранить этот код на С#, но, пожалуйста, помните, что по сравнению с несколькими компиляторами C CLR (как и все другие виртуальные машины, которые я знаю) мало помогает оптимизировать сгенерированный код.

Ответ 2

Обновлено:

Что произойдет, если вы напишете внутренний цикл, чтобы учесть локальность ссылки:

for (int i = 0; i < length1; i++) 
{ 
    s = GetS(i); 
    c = GetC(i); 
    double[] omegaTemp = omega[i]; 

    for(int j = 0; j < length2; j++) 
    { 
        double oldU = u[j]; 
        u[j] = c * oldU + s * omegaTemp[j]; 
        omegaTemp[j] = c * omegaTemp[j] - s * oldU; 
    } 
} 

Ответ 3

Используйте блок unsafe и указатели для индексации в массив omega. Это позволит устранить накладные расходы на проверку диапазона и может стать значительной победой, если вы выполняете достаточно доступа. Также много времени можно потратить на функции GetS() и GetC(), для которых вы не предоставили источник.

Ответ 4

Очень маловероятно, что запуск этого на родном C/С++ "автоматически" ускорит работу. Если вы хорошо разбираетесь в SIMD (и length1 и length2 достаточно велики, что вызов P/Invoke не имеет значения), возможно, вы могли бы что-то сделать.

Но единственный способ узнать наверняка - попробовать его и профиль.

Ответ 5

Вы можете попытаться использовать Mono.Simd для более эффективного использования CPU.

http://tirania.org/blog/archive/2008/Nov-03.html

Как сказано, многое можно получить на С#, вручную извлекая повторяющиеся утверждения из циклов.

var outsideAddr0 = outsideGeneratedAddress[0];
var outsideAddr1 = outsideGeneratedAddress[1];
var outsideAddr2 = outsideGeneratedAddress[2];
var outsideAddr3 = outsideGeneratedAddress[3];
var outsideAddr4 = outsideGeneratedAddress[4];
var outsideAddr5 = outsideGeneratedAddress[5];
var outsideAddr6 = outsideGeneratedAddress[6];
var outsideAddr7 = outsideGeneratedAddress[7];
var outsideAddr8 = outsideGeneratedAddress[8];
var outsideAddr9 = outsideGeneratedAddress[9];
for (int i = 0; i < length1; i++)
{
  var omegaAtI = omega[i];
  double aa = 
   omegaAtI[outsideAddr0]
   + omegaAtI[outsideAddr1]
   + omegaAtI[outsideAddr2]
   + omegaAtI[outsideAddr3]
   + omegaAtI[outsideAddr4]
   + omegaAtI[outsideAddr5]
   + omegaAtI[outsideAddr6]
   + omegaAtI[outsideAddr7]
   + omegaAtI[outsideAddr8]
   + omegaAtI[outsideAddr9];

  double alphaOld = alpha;
  alpha = Math.Sqrt(alpha * alpha + aa * aa);

  var s = -aa / alpha;
  var c = alphaOld / alpha;

  for(int j = 0; j <= i; j++)
  {
    double oldU = u[j];
    var omegaAtIJ = omegaAtI[j];
    u[j] = c * oldU + s * omegaAtIJ;
    omegaAtI[j] = c * omegaAtIJ  - s * oldU;
  }
}

Ответ 6

Просто использование C или С++ не даст вам большого увеличения скорости, если оно есть. У вас также есть накладные расходы на вызов в подпрограмме C, но не огромное влияние, если вы не делаете это много раз в цикле.

Сначала попробуйте другие вещи на С#. Если переменные являются поплавками, а не удваиваются, это замедляет вычисления. Также, как сказал Радж, использование параллельного программирования даст вам большой импульс скорости.

Ответ 7

.net-взаимодействие с неуправляемым кодом очень медленное. Вы можете использовать все преимущества неуправляемой памяти, просто используя систему api для распределения неуправляемой памяти.

Вы можете вызвать VirtualAlloc для выделения страниц памяти, а затем вызвать VirtualProtect, чтобы связать их напрямую с ОЗУ без обмена.

Этот подход позволяет выполнять вычисления на большом количестве данных по крайней мере в 3 раза быстрее, чем вы могли бы сделать это в управляемой памяти.

Ответ 9

Для простой 64-разрядной арифметики в Java я видел около 33% ускорения (от 23 нс до 16 нс) при переносе на C и возиться с флагами оптимизации (-fprofile-generate, -fprofile-use). Это может стоить того.

Другое дело, что omega [i] [j] делает вид, что omega - это массив массивов - вы можете получить лучшую производительность с двухмерным массивом (я думаю, что синтаксис - это что-то вроде omega [i, j], но я забыл, как вы его выделяете).

Ответ 10

Очень сомневаюсь в этом. Внутренний цикл, который обрабатывает примитивные типы и не выделяет память, будет очень эффективен в С#. Нативный байт-код будет генерироваться один раз от IL, поэтому не должно быть много управляемых служебных данных.

Учитывая, что это довольно маленькая функция, вы можете профилировать оба и посмотреть, есть ли разница.

Ответ 11

Также рассмотрите стоимость сортировки данных между управляемыми и нативными вызовами. С# имеет довольно быструю скорость выполнения. Вы также можете NGEN сборку для генерации собственных изображений сборки для более быстрого выполнения.

Ответ 12

Я понятия не имею, насколько это практично, но подумали ли вы о попытке запустить это на GPU? Возможно, используя что-то вроде OpenCL или DirectCompute?

Зависимости и квадратный корень могут вас убьют, но в наши дни графические процессоры имеют на порядок более высокую производительность с плавающей запятой, чем процессоры.