2013-07-01 5 views
8

Прежде всего, я знаю, что этот вопрос действительно звучит так, как будто я не искал, но я сделал, много.Выполнение чертежа C# mandelbrot более эффективно

Я написал небольшой код чертежа Мандельброта для C#, это в основном форма окна с PictureBox, на которой я рисую набор Мандельброта.

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

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

Я сделал следующие вещи, чтобы оптимизировать его:

  • Вместо использования методов SetPixel GetPixel на растровый объект, я использовал метод LockBits писать непосредственно в памяти, который сделал вещи намного быстрее.

  • Вместо использования сложных числовых объектов (с классами, которые я сделал сам, а не со встроенными), я эмулировал комплексные числа, используя 2 переменные, re и im. Это позволило мне сократить количество умножений, потому что возведение в квадрат реальной части и воображаемой части - это то, что выполняется несколько раз во время вычисления, поэтому я просто сохраняю квадрат в переменной и повторно использую результат без необходимости его пересчета.

  • Я использую 4 нити для рисования Мандельброта, каждая нить выполняет разную четверть изображения, и все они работают одновременно. Как я понял, это означает, что мой процессор будет использовать 4 его ядра для рисования изображения.

  • Я использую алгоритм Escape Time Algorithm, который, как я понял, является самым быстрым?

Вот мой, как я двигаюсь между пикселями и вычислить, он прокомментировал так, я надеюсь, что это понятно:

 //Pixel by pixel loop: 
     for (int r = rRes; r < wTo; r++) 
     { 
      for (int i = iRes; i < hTo; i++) 
      { 

       //These calculations are to determine what complex number corresponds to the (r,i) pixel. 
       double re = (r - (w/2))*step + zeroX ; 
       double im = (i - (h/2))*step - zeroY; 

       //Create the Z complex number 
       double zRe = 0; 
       double zIm = 0; 

       //Variables to store the squares of the real and imaginary part. 
       double multZre = 0; 
       double multZim = 0; 

       //Start iterating the with the complex number to determine it's escape time (mandelValue) 
       int mandelValue = 0; 
       while (multZre + multZim < 4 && mandelValue < iters) 
       { 
        /*The new real part equals re(z)^2 - im(z)^2 + re(c), we store it in a temp variable 
        tempRe because we still need re(z) in the next calculation 
         */ 
        double tempRe = multZre - multZim + re; 

        /*The new imaginary part is equal to 2*re(z)*im(z) + im(c) 
         * Instead of multiplying these by 2 I add re(z) to itself and then multiply by im(z), which 
         * means I just do 1 multiplication instead of 2. 
         */ 
        zRe += zRe; 
        zIm = zRe * zIm + im; 

        zRe = tempRe; // We can now put the temp value in its place. 

        // Do the squaring now, they will be used in the next calculation. 
        multZre = zRe * zRe; 
        multZim = zIm * zIm; 

        //Increase the mandelValue by one, because the iteration is now finished. 
        mandelValue += 1; 
       } 


       //After the mandelValue is found, this colors its pixel accordingly (unsafe code, accesses memory directly): 
       //(Unimportant for my question, I doubt the problem is with this because my code becomes really slow 
       // as the number of ITERATIONS grow, this only executes more as the number of pixels grow). 
       Byte* pos = px + (i * str) + (pixelSize * r); 
       byte col = (byte)((1 - ((double)mandelValue/iters)) * 255); 
       pos[0] = col; 
       pos[1] = col; 
       pos[2] = col; 

      } 
     } 

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

Прямо сейчас есть 2 способа, я знаю, что могу улучшить его:

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

  2. Я могу переместить обработку на GPU. Я не знаю, как это сделать (OpenGL, может быть, DirectX - это даже так просто или мне нужно будет многому научиться?). Если кто-то может отправить мне ссылки на соответствующие учебные пособия по этому вопросу или сказать мне в целом об этом, это было бы здорово.

Большое спасибо за чтение, что далеко, и надеюсь, что вы можете мне помочь :)

+0

float обычно быстрее, хотя я думаю, что это зависит от того, какой процессор вы используете. float обычно быстрее, чем двойной, если вы используете gpu. – sav

ответ

1

Для перемещения обработки на GPU, у вас есть много прекрасных примеров здесь:

https://www.shadertoy.com/results?query=mandelbrot

Обратите внимание, что для просмотра этой ссылки необходим браузер с поддержкой WebGL. Лучше всего работает в Chrome.

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

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

Другое предложение состоит в том, что при использовании нескольких потоков вы должны следить за тем, чтобы каждый поток не читал/не записывал память других потоков, так как это приведет к конфликтам в кеше и ухудшит производительность. Один хороший алгоритм может быть разделен на работу в линиях сканирования (вместо четырех кварталов, как и сейчас). Создайте несколько потоков, а затем, пока линии, оставшиеся для обработки, назначьте строку сканирования доступному потоку. Пусть каждый поток записывает пиксельные данные в локальную часть памяти и копирует их обратно в основное растровое изображение после каждой строки (чтобы избежать столкновений с кешем).

+0

Большое спасибо за то, что нашли время, чтобы ответить :) О графическом процессоре примеры не помогают мне, потому что я не имею абсолютно никакого представления об этой теме, как она работает и какие вычисления может делать GPU (или как он вообще доступен?). Сначала я надеялся на что-то с базовой информацией. О дальнейших оптимизациях, я не прочь прочесть код. Масштабирование с низким разрешением - это то, что я рассмотрел, но я надеялся, что, возможно, были другие вещи, которые я могу сделать в первую очередь. – Omer

+0

О столкновениях с кешем: я действительно не понимаю, почему возникли столкновения с кешем? Если я убеждаюсь, что каждый поток записывается точно в память, он должен был бы иметь коллизии кеша?Почему строки сканирования лучше (это не просто другой способ разделить изображение?) – Omer

+0

@ Омер Scanline хороши тем, что они являются одним непрерывным блоком в памяти, что опять-таки полезно для кэша CPU. Всегда лучше писать в непрерывной памяти (поэтому лучше перемещать пиксели в порядке y/x вместо x/y). Столкновения возникают из-за перекрытия кешей, у нескольких потоков может быть одна и та же 4096 (скажем) байт-памяти в кеше, поэтому они сталкиваются даже тогда, когда они пишут разные части этой памяти. –

2

WRT, кодирующий графический процессор, вы можете посмотреть на Cudafy.Net (он также не содержит OpenCL, который не привязан к NVidia), чтобы начать понимать, что происходит, и, возможно, даже делать все, что вам нужно. Я быстро нашел его - и мою графическую карту - не подходит для моих нужд, но для Мандельброта на том этапе, на котором вы находитесь, все должно быть хорошо.

Вкратце: вы кодируете графический процессор со вкусом C (Cuda C или OpenCL обычно), затем нажмите «ядро» (ваш скомпилированный метод C) на графический процессор, за которым следуют любые исходные данные, а затем вызовите, что « ядро ", часто с параметрами, чтобы сказать, какие данные использовать - или, возможно, несколько параметров, чтобы указать, где разместить результаты в своей памяти.

Когда я делал фрактальную рендеринг самостоятельно, я избегал рисовать в растровое изображение по причинам, уже изложенным и отложенным фазе рендеринга. Кроме того, я склонен писать массивный многопоточный код, что очень плохо для попытки получить доступ к растровому изображению. Вместо этого я пишу в общий магазин - совсем недавно я использовал MemoryMappedFile (встроенный класс .Net), так как это дает мне довольно приличную скорость доступа и огромную адресуемую область. Я также склонны записывать свои результаты в очередь и иметь другой поток, посвященный передаче данных на хранение; времена вычислений каждого пикселя Мандельброта будут «оборваны», то есть они не всегда будут иметь одинаковый период времени. В результате ваша фиксация пикселей может быть узким местом для очень низких значений итераций. Объединяя его в другой поток, ваши вычислительные потоки никогда не ожидают завершения хранения.

В настоящее время я играю с визуализацией Buddhabrot набора Мандельброта, рассматривая использование графического процессора для масштабирования рендеринга (поскольку он занимает очень много времени с процессором) и имеет огромный набор результатов. Я думал о том, чтобы настроить 8-гигапиксельное изображение, но я пришел к пониманию, что мне нужно отклоняться от ограничений пикселей и, возможно, от арифметики с плавающей запятой из-за проблем с высокой точностью.Мне также придется покупать новое оборудование, поэтому я могу взаимодействовать с GPU по-другому - разные задания вычислений будут завершены в разное время (как и в предыдущем комментарии к итерации), поэтому я не могу просто запускать партии потоков и ждать для их всех, чтобы завершить, не тратя много времени на ожидание особо особого количества итераций из всей партии.

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

+0

Мысль, что набор мандлбротов не был симметричным, -> хаотический – sav

+0

http://kluge.in-chemnitz.de/documents/fractal/node9.html Ответы есть там :) Хаос не означает Random, есть высокая степень предсказуемости в наборе Мандельброта. – user1796307

3

Если вы решите переместить обработку в gpu, вы можете выбрать один из нескольких вариантов. Поскольку вы используете C#, XNA позволит вам использовать HLSL. RB Whitaker имеет самые простые руководства по XNA, если вы выберете эту опцию. Другой вариант - OpenCL. OpenTK поставляется с демо-программой фрактала julia set. Это было бы очень просто изменить, чтобы отобразить набор mandlebrot. См. here Не забудьте найти шейдер GLSL, который идет с исходным кодом.

О ГПУ, примеры не являются подспорьем для меня, потому что у меня нет абсолютно представления об этой теме, как это даже работает, и какие вычислений GPU может сделать (или, как его еще доступен?)

Различное программное обеспечение GPU работает по-разному, однако ...

Обычно программист будет написать программу для GPU на языке шейдеров, таких как HLSL, GLSL или OpenCL. Программа, написанная на C#, будет загружать шейдерный код и компилировать его, а затем использовать функции в API для отправки задания на GPU и последующего получения результата.

Посмотрите на FX Composer или визуализируйте обезьяну, если вы хотите попрактиковаться в шейдерах, не беспокоясь о API.

Если вы используете HLSL, конвейер рендеринга выглядит следующим образом.

pipeline

вершинный шейдер отвечает за принятие точек в 3D-пространстве и вычисляя их положение в 2D-поле обзора. (Не очень важно для вас, так как вы работаете в 2D)

Шейдер пикселя отвечает за применение эффектов шейдера к пикселям после выполнения вершинного шейдера.

OpenCL - это другая история, ориентированная на вычисления GPU общего назначения (то есть: не только графика). Его более мощный и может использоваться для графических процессоров, DSP и суперкомпьютеров.

Смежные вопросы