Инициализация процесса 1

advertisement
Министерство образования и науки Российской Федерации
Федеральное государственное бюджетное образовательное
учреждение высшего профессионального образования
«Самарский государственный аэрокосмический университет
имени академика С.П. Королева
(национальный исследовательский университет)»
Факультет информатики
Кафедра технической кибернетики
Выпускная квалификационная работа
магистра
на тему
Применение алгоритмов дискретного преобразования
Фурье для решения задачи формирования модели водной
поверхности
Выпускник _____________________________________ Князев В.А.
(подпись)
Руководитель работы _____________________________ Чичева М.А.
(подпись)
Нормоконтролёр _________________________________ Суханов С.В.
(подпись)
Рецензент ______________________________________
(подпись)
САМАРА 2012
Министерство образования и науки Российской Федерации
Федеральное государственное бюджетное образовательное
учреждение высшего профессионального образования
«Самарский государственный аэрокосмический университет
имени академика С.П. Королева
(национальный исследовательский университет)»
Факультет информатики
Кафедра технической кибернетики
«УТВЕРЖДАЮ»
Заведующий кафедрой
____________________________
«___»_______________ 20____ г.
ЗАДАНИЕ НА
ВЫПУСКНУЮ КВАЛИФИКАЦИОННУЮ РАБОТУ
МАГИСТРА
студенту М627 группы Князеву Виталию Александровичу
1. Тема работы Применение алгоритмов дискретного преобразования Фурье
для решения задачи формирования модели водной поверхности
утверждена приказом по университету от «25» апреля 2012 г. № 164-СТ.
2. Исходные данные к работе:
Методы и алгоритмы последовательного и параллельного вычисления
дискретных преобразований Фурье, подход к моделированию водной
поверхности.
3. Перечень вопросов, подлежащих разработке:
2
3.1. Изучение способов построения 3D-модели водной поверхности
3.2 Изучение методов вычисления многомерных дискретных ортогональных
преобразований.
3.3. Создание исследовательского программного комплекса.
3.4. Проведение вычислительных экспериментов и анализ результатов.
Срок представления законченной работы «___» __________ 20___ г.
Руководитель работы ______________________
Чичева М.А.
(подпись)
Задание принял к исполнению «___» __________ 20___ г.
______________
(подпись)
3
Князев В.А.
РЕФЕРАТ
Выпускная квалификационная работа магистра: 113 c., 30 рисунков,
15 таблиц, 13 источников, 3 приложения.
Презентация: 14 слайдов Microsoft PowerPoint.
ДИСКРЕТНОЕ ПРЕОБРАЗОВАНИЕ ФУРЬЕ, 3D-МОДЕЛЬ ВОДНОЙ
ПОВЕРХНОСТИ,
БЫСТРЫЕ
АЛГОРИТМЫ,
ПАРАЛЛЕЛЬНЫЕ
АЛГОРИТМЫ, ГИПЕРКОМПЛЕКСНАЯ АЛГЕБРА, ПРЯМАЯ СУММА
КОМПЛЕКСНЫХ АЛГЕБР, JAVA, CUDA
Объектом исследования являются алгоритмы дискретного преобразования
Фурье и методы их распараллеливания.
Цель работы – исследование и сравнительный анализ параллельных
алгоритмов дискретного преобразования Фурье (ДПФ) в задаче построения 3Dмодели водной поверхности.
Разработана программная реализация рассмотренных алгоритмов и
программного комплекса, строящего 3D-модель водной поверхности.
Проведены
вычислительные
эксперименты
по
быстродействию
алгоритмов преобразования Фурье как многоядерном центральном процессоре
(CPU), так и на графическом процессоре (GPU). Произведен сравнительный
анализ полученных результатов.
4
СОДЕРЖАНИЕ
ОПРЕДЕЛЕНИЯ, ОБОЗНАЧЕНИЯ И СОКРАЩЕНИЯ ......................................... 8
ВВЕДЕНИЕ .................................................................................................................. 9
1
2
Модель водной поверхности океана ................................................................. 11
1.1
Волны Герстнера .......................................................................................... 11
1.2
Статистическая модель ................................................................................ 14
Алгоритмы дискретного преобразования Фурье и способы их
распараллеливания .................................................................................................... 18
2.1
Используемые алгебраические структуры................................................. 18
2.1.1
Алгебра гиперкомплексных чисел ....................................................... 18
2.1.2
Прямая сумма комплексных алгебр ..................................................... 22
2.2
Одномерное ДПФ ......................................................................................... 25
2.3
Двумерное ДПФ............................................................................................ 30
2.3.1
Построчно-столбцовый алгоритм ДПФ ............................................. 30
2.3.2
Алгоритм двумерного ДПФ по основанию 2 ...................................... 31
2.3.3
Учет симметрий спектра вещественного сигнала .............................. 35
2.4
Способы распараллеливания ....................................................................... 37
2.4.1
По компонентам в построчно-столбцовом алгоритме ....................... 37
2.4.2
Распараллеливание по структуре декомпозиции для
двумерного случая .............................................................................................. 38
2.4.3
Распараллеливание за счет структуры алгебры для
двумерного случая .............................................................................................. 39
3
Программная реализация алгоритмов .............................................................. 42
3.1
Описание технологии Cuda ......................................................................... 42
5
3.2
Описание библиотеки JCuda ....................................................................... 46
3.3
Исследовательский программный комплекс формирования модели
водной поверхности............................................................................................... 48
3.4
Общие сведения для алгоритмов двумерного случая ............................... 51
3.5
Программная реализация последовательного алгоритма
двумерного ДПФ .................................................................................................... 52
3.6
Программная реализация последовательного построчно-столбцового
алгоритма двумерного ДПФ ................................................................................. 54
3.7
Программная реализация алгоритма двумерного ДПФ с
распараллеливанием за счет структуры алгебры ............................................... 56
3.8
Программная реализация алгоритма двумерного ДПФ с
распараллеливанием по структуре декомпозиции ............................................. 58
3.9
Программная реализация гибридного алгоритма двумерного ДПФ с
распараллеливанием по структуре декомпозиции и распараллеливанием по
структуре алгебры.................................................................................................. 61
3.10
Программная реализация ДПФ с постоянной синхронизацией
процессов и без ...................................................................................................... 64
4
Экспериментальные исследования алгоритмов и анализ результатов ......... 68
4.1
Исследование скорости выполнения алгоритмов на CPU ....................... 69
4.2
Исследование скорости выполнения алгоритмов на GPU ....................... 77
4.3
Исследование скорости прорисовки модели водной поверхности ......... 81
ЗАКЛЮЧЕНИЕ ......................................................................................................... 84
СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ ............................................... 86
ПРИЛОЖЕНИЕ А Графики сравнения алгоритмов ДПФ .................................... 88
ПРИЛОЖЕНИЕ Б Примеры работы программы для разных размеров сетки .... 91
6
ПРИЛОЖЕНИЕ В Исходный текст программы .................................................... 93
7
ОПРЕДЕЛЕНИЯ, ОБОЗНАЧЕНИЯ И СОКРАЩЕНИЯ
ДПФ – дискретное преобразование Фурье.
ГДПФ – дискретное преобразование в алгебре гиперкомплексных чисел.
CPU – центральный процессор (англ., Central Processing Unit).
GPU – графический процессор (англ., Graphics Processing Unit).
FPS – число кадров в секунду (англ., Frames per Second).
8
ВВЕДЕНИЕ
Преобразование Фурье является мощным инструментом, применяемым в
различных научных областях. Оно используется при изучении колебательных
процессов. Преобразование Фурье можно использовать как средство решения
сложных
уравнений,
описывающих
динамические
процессы,
которые
возникают под воздействием электрической, тепловой или световой энергии. В
других случаях оно позволяет выделять регулярные составляющие в сложном
колебательном сигнале, благодаря чему можно правильно интерпретировать
экспериментальные наблюдения в астрономии, медицине и химии. Всё это
вызывает большой интерес к этому преобразованию.
Однако само дискретное преобразование Фурье выполняется очень
медленно, и обработка большого входного сигнала занимает долгое время.
Поэтому на смену «прямому» преобразованию пришли быстрые алгоритмы,
которые уменьшают вычислительную сложность преобразования [1]. К тому же
их
выполнение
можно
разбить
между
несколькими
процессорами/компьютерами, добившись тем самым параллельной реализации.
Затем, около 20 лет назад, появились первые работы, в которых одним из
подходов к синтезу быстрых алгоритмов ДПФ являлось погружение данных в
специальные
алгебраические
структуры,
что
позволило
уменьшить
вычислительную сложность, особенно в случае вещественных входных данных
[2].
Сфера применения дискретного преобразования Фурье очень широка. Оно
играет важную роль в физике плазмы и полупроводниковых материалов,
химии, микроволновой акустике, медицинских обследованиях, радиолокации,
сейсмологии, океанографии и 3D-моделировании.
Построение 3D-модели водной поверхности океана – типичная задача,
которая ставится как в научных целях (океанология), так и в сферах
кинематографии и разработки интерактивных 3D-приложений и видеоигр, где
9
основными критериями оценки качества модели является её реалистичность и
скорость построения.
В
данной
работе
проводится
исследование
быстрых
алгоритмов
двумерного ДПФ при построении 3D-модели поверхности океана. Были
реализованы как «обычные» алгоритмы (построчно-столбцвое преобразование,
алгоритм Кули-Тьюки) в пространстве комплексных чисел, так и алгоритмы с
погружением
входного
сигнала
в
четырехмерную
алгебру
(алгебра
гиперкомплексных чисел, прямая сумма комплексных алгебр).
В разделе 1 ставится задача для данной работы, а также приводится
описание двух алгоритмов построения модели водной поверхности.
Теоретические сведения об алгебраических структурах и описание
алгоритмов преобразования Фурье приводятся в разделе 2.
В разделе 3 описываются реализации алгоритмов, а также используемые
технологии.
С помощью реализованной программы были исследованы как скорость
выполнения самих алгоритмов, так и скорость построения 3D-модели. Эти
результаты приведены в разделе 4.
10
1 Модель водной поверхности океана
Вода в компьютерной графике стала распространенным инструментом в
области визуальных эффектов: начиная обычными иллюстрациями с заранее
отрисованной моделью и заканчивая художественными фильмами, в которых
присутствует еще и анимация [3]. Компании, занимающиеся разработкой
продуктов в сфере 3D-графики, продолжают расширять и совершенствовать эти
инструменты, стремясь к более качественным с точки зрения геометрии
формам поверхности, сложным взаимодействиям и более убедительному виду,
что одновременно усложняет саму генерацию волн. Поскольку нужную форму
волн, которую хотят видеть океанологи или художники в 3D-пакетах, или
программисты,
разрабатывающие
интерактивное
приложение,
задают
множество параметров (высота и скорость волн, направление их движения,
также направление и сила ветра и другие), то задача построения 3D-модели
поверхности воды становится актуальной. А в задачах, где её еще необходимо и
анимировать, важную роль играет не только реалистичность модели, но и
скорость её построения.
В этом разделе будут рассмотрены два алгоритма по созданию карты
высот, используемой для построения морских волн. Вначале будет рассмотрен
наиболее простой метод, когда волны образуются суммированием синусоид,
потом будет рассмотрен метод создания модели на основе быстрого
преобразования Фурье.
1.1 Волны Герстнера
Волны Герстнера были впервые найдены около 200 лет назад в качестве
приближенного
решения
гидродинамических
уравнений.
Свое
первое
применение в компьютерной графике они нашли в 1982 году Фурье и Ривзом.
Эта физическая модель описывает поверхность с точки зрения движения её
отдельных точек[4]. При внимательном рассмотрении, можно заметить, что эти
11
точки движутся по кругу, причем так, словно через них как раз проходит волна
(см. рисунок 1).
Рисунок 1 – Движение точки поверхности
Если точку на невозмущенной поверхности пометить как 𝑋0 = (𝑥0 , 𝑦0 ), а
её высоту как 𝑍0 = 0, то её волновое движение в момент времени 𝑡 можно
описать в следующем виде
⃗
𝑘
⃗⃗⃗⃗⃗⃗⃗⃗
⃗ · ⃗⃗⃗⃗
𝑋(𝑡) = ⃗⃗⃗⃗
𝑋0 − ( ) 𝐴𝑠𝑖𝑛(𝑘
𝑋0 − 𝜔𝑡);
𝑘
⃗ · ⃗⃗⃗⃗
𝑦 = 𝐴 𝑐𝑜𝑠(𝑘
𝑋0 − 𝜔𝑡),
⃗ – это горизонтальный вектор («волновой вектор»), значением
где 𝑘
которого является направление движения волны;
𝐴 – это амплитуда волны;
𝑘 – величина, зависящая от длины волны как 𝑘 =
2𝜋
𝜆
;
𝜔 – частота, относящаяся к волновому вектору. Она будет
рассмотрена ниже.
Как видно, волны Герстнера весьма ограничены, поскольку они
представлены
одной синусоидальной волной по горизонтали и вертикали.
12
Более сложный профиль может быть получен путем суммирования набора
синусоид. Выбрав набор волновых векторов 𝑘𝑖 , амплитуд 𝐴𝑖 и начальных фаз
𝜑𝑖 , для 𝑖 = 1, … 𝑁 получаем выражения для результирующей волны
𝑁
⃗⃗⃗
𝑘𝑖
⃗⃗⃗⃗⃗⃗⃗⃗
⃗⃗⃗𝑖 ∙ ⃗⃗⃗⃗
⃗⃗⃗⃗
𝑋(𝑡) = 𝑋0 – ∑ ( ) 𝐴𝑖 𝑠𝑖𝑛(𝑘
𝑋0 − 𝜔𝑖 𝑡 + 𝜑𝑖 ) ;
𝑘𝑖
𝑖=1
𝑁
⃗⃗⃗𝑖 ∙ 𝑋
⃗⃗⃗⃗0 − 𝜔𝑖 𝑡 + 𝜑𝑖 ).
𝑦 = ∑ 𝐴𝑖 𝑐𝑜𝑠(𝑘
𝑖=1
На рисунке 2 представлен профиль волны, полученный путем сложения 4
синусоид.
Рисунок 2 – Волна, полученная путем суммирования 4 простых синусоидальных волн
13
Анимация волн Герстнера определяется набором частот, для каждой
компоненты из множества. Для волн на водной поверхности известна связь
между этими частотами и величиной соответствующих волновых векторов ⃗⃗⃗
𝑘𝑖
⃗ ) = 𝑔𝑘
⃗,
𝜔2 (𝑘
где g – это гравитационная постоянная, значение которой берут равным
9,8 м⁄сек2 .
К сожалению, модель Герстнера не дает необходимого качества модели,
поскольку в волнах наблюдается периодичность, а само поведение волн
позволяют использовать эту модель лишь для водоемов со спокойной
поверхностью (озера, реки). Для анимирования поверхности с более сильными
волнами применяется статистическая модель.
1.2 Статистическая модель
В физической океанографии разработан метод, позволяющий построить
волновую картину исходя из атмосферных условий (скорости ветра, его
направления и т.п.) не прибегая к решению уравнений движения, но лишь
основываясь на экспериментальных наблюдениях и их статистической
обработке и анализе. Этот метод так же, как и подход Герстнера, основан на
предположении, что свободную поверхность с некоторыми допущениями
можно представить в виде суммы гармоник различных длин и периодов [3]. В
статистическом подходе для анализа их характеристик используется мощный
математический аппарат
дискретных
преобразований Фурье, который
позволяет на основании экспериментальных данных (в частности, измерений
высоты волн через некоторый интервал времени), специальным образом
сглаженных и обработанных, получить некоторое спектральное распределение,
характерное для данных атмосферных условий. Эти распределения, в свою
очередь, подвергаются анализу и аппроксимации, что позволяет построить
некоторые аналитические приближения для спектра плотности мощности
14
волновой функции, которые получили название океанских спектров. В
компьютерной графике ставится обратная задача – получить распределение
высот океанской поверхности, исходя из выбранного спектра и атмосферных
условий. Это возможно при помощи обратного преобразования Фурье,
примененного к специальным образом обработанному полю случайных чисел.
Как уже говорилось, в основе статистической модели синтеза поверхности
океана лежит дискретное преобразование Фурье. Волна представляется как
сумма большого числа гармоник (т.е. фактически осуществлялось разложение в
ряд Фурье по пространственным переменным 𝑥 и 𝑦).
Производя ДПФ над комплексной сеткой можно получить значения
амплитуд волн в её узлах, то есть карту высот. Полученная с помощью
преобразования Фурье карта высот периодична и её можно “натягивать” на
сколь угодно большую поверхность. Соответственно, чем больше размер сетки
для преобразования Фурье, тем больше полученная карта высот, тем более
сложная поверхность и менее заметна периодичность волн, что сильно
увеличивает реалистичность. Таким образом, карта высот статистической
модели синтеза водной поверхности будет описываться следующей формулой
⃗ , 𝑡)𝑒 𝑖𝑘⃗𝑥 ,
ℎ(𝑥 , 𝑡) = ∑ ℎ̃(𝑘
⃗
𝑘
𝑛𝐿𝑥 𝑚𝐿𝑦
где 𝑥 = (
𝑁
,
𝑀
);
⃗ = (𝑘𝑥 , 𝑘𝑦 ), 𝑘𝑥 = 2𝜋𝑛 , 𝑘𝑦 = 2𝜋𝑚;
𝑘
𝐿𝑥
𝐿𝑦
(𝐿𝑥 , 𝐿𝑦 ) – размеры фрагмента водной поверхности;
(𝑁, 𝑀) – размеры сетки для преобразования Фурье;
−
𝑀
2
<𝑚<
𝑀
2
𝑁
𝑁
2
2
,− < 𝑛 < .
Чтобы синтезировать реалистичную водную поверхность необходимо
подобрать способ выбора коэффициентов для преобразования Фурье. Также
15
необходимо решить вопрос о плавной анимации водной поверхности со
временем.
Коэффициенты ДПФ выбираются исходя из спектра Филлипса
⃗)=𝐴
𝑃ℎ (𝑘
где 𝐿 =
𝑉2
𝑔
exp (−
𝑘
1
)
(𝑘𝐿)2 ⃗
⃗ |,
|𝑘 ∙ 𝜔
4
– это максимально возможная волна, возникающая под
действием непрерывного ветра со скоростью 𝑉 [5];
𝜔
⃗ – это сила и направление ветра;
𝐴 – нормирующая константа.
Начальный спектр водной поверхности выражается в следующем виде
⃗) =
ℎ̃0 (𝑘
1
√2
⃗) ,
(𝜉𝑟 + 𝑖𝜉𝑖 )√𝑃ℎ (𝑘
где 𝜉𝑟 , 𝜉𝑖 – это случайные числа, полученные по закону нормального
распределения.
Общая диаграмма генерации начального спектра показана на рисунке 3.
16
Рисунок 3 – Диаграмма генерации начального спектра
В дальнейшем синтезировать спектр Филлипса каждый кадр не нужно,
достаточно ”анимировать” спектр с фиксированным по времени периодом.
⃗ , 𝑡) = ℎ̃0 (𝑘
⃗ )𝑒 𝑖𝜔(𝑘)𝑡 + ℎ̃0∗ (−𝑘
⃗ )𝑒 −𝑖𝜔(𝑘)𝑡 ,
ℎ̃(𝑘
где 𝜔(𝑘) = √𝑔𝑘;
ℎ̃0∗ – число, сопряженное к ℎ̃0 .
Можно заметить, что коэффициенты преобразования Фурье комплексные,
хотя высота волны представляется вещественным числом. На самом деле,
комплексная компонента не лишняя, она означает фазу волны. Формула
⃗ , 𝑡)𝑒 𝑖𝑘⃗𝑥 как раз и описывает сдвиг фазы волны. Таким образом,
ℎ(𝑥 , 𝑡) = ∑𝑘⃗ ℎ̃(𝑘
сдвиг фаз всех гармоник дает плавную, и в то же время, непредсказуемую,
анимацию поверхности.
17
2 Алгоритмы дискретного преобразования Фурье и способы
их распараллеливания
В данном разделе содержатся сведения об используемых алгебраических
структурах: гиперкомплексной алгебре и прямой сумме комплексных алгебр, а
также сведения об исследуемых алгоритмах.
2.1 Используемые алгебраические структуры
2.1.1 Алгебра гиперкомплексных чисел
Приведем определение алгебры гиперкомплексных чисел [6]:
2𝑑 -мерная 𝑅-algebra 𝑩𝒅 с базисом
𝛼
𝜦 = {∏ 𝜀𝑖 𝑖 ,
𝛼𝑖 ∈ {0; 1}; 𝐼 ∈ {1, … , 𝑑}}
𝑖∈𝐼
(1)
называется коммутативно-ассоциативной гиперкомплексной алгеброй. Здесь
𝜀1 , 𝜀2 , … , 𝜀𝑑 – базис -мерного векторного пространства 𝑽, 𝜀𝑖0 = 1, 𝜀𝑖1 = 𝜀𝑖 , а их
правило умножения задается соотношениями
𝜀𝑖 𝜀𝑗 = 𝜀𝑗 𝜀𝑖 ,
𝜀𝑖2 = 𝛽𝑖 ,
𝑖, 𝑗 ∈ 𝐼,
𝛽𝑖 = ±1 .
(2)
Произвольный элемент 𝑔 ∈ 𝑩𝑑 имеет вид
𝑔 = 𝜉0 𝐸0 + ⋯ + 𝜉2𝑑−1 𝐸2𝑑−1 = ∑ 𝜉𝑡 𝐸𝑡 ,
𝑡∈𝑇
(3)
где
𝛼
𝐸𝑡 = ∏ 𝜀𝑖 𝑖 ,
𝑖∈𝐼
𝑡 = ∑ 𝛼𝑖 2𝑖−1 ∈ 𝑇 = {0,1, … , 2𝑑 − 1}.
𝑖∈𝐼
(4)
18
В этой алгебре сложение элементов выполняется покомпонентно, а
умножение
полностью
определяется
правилами
умножения
базисных
элементов
𝐸𝑡 𝐸𝜏 = 𝛹(𝑡, 𝜏)𝐸𝑡⊕𝜏 ,
∀𝑡, 𝜏 ∈ 𝑇 ,
(5)
где ⊕ обозначает побитное сложение по модулю 2,
ℎ (𝑡,𝜏)
𝛹(𝑡, 𝜏) = ∏ 𝛽𝑖 𝑖
,
ℎ𝑖 (𝑡, 𝜏) = 𝛼𝑖 𝛼𝑖′ ,
𝜏 = ∑ 𝛼𝑖 2𝑖−1 .
𝑖∈𝐼
𝑖∈𝐼
(6)
Здесь мы рассматриваем только такую гиперкомплексную алгебру, которая
изоморфна прямой сумме комплексных алгебр
𝑩𝑑 ≅ ⏟
𝑪 ⊕ 𝑪 ⊕ …⊕ 𝑪 ,
2𝑑−1
(7)
что гарантирует минимальную сложность операции умножения в 𝑩𝑑 . В
этом случае, по крайней мере, одному элементу 𝛼𝑖 соответствует 𝛽𝑖 = −1.
В данной работе исследуется случай для 𝑑 = 2. Рассмотрим его.
В этом случае 𝑩2 является четырехмерной коммутативно-ассоциативной
алгеброй, произвольный элемент ℎ ∈ 𝑩2 которой имеет вид
ℎ = 𝑎𝐸0 + 𝑏𝐸1 + 𝑐𝐸2 + 𝑑𝐸3 ,
𝑎, 𝑏, 𝑐, 𝑑 ∈ 𝑹 ,
где 𝐸0 = 1, 𝐸1 = 𝜀1 , 𝐸2 = 𝜀2 , 𝐸3 = 𝜀1 𝜀2 .
Представим ℎ в следующем виде
ℎ = 𝑎 + 𝑏𝜀1 + 𝑐𝜀2 + 𝑑𝜀1 𝜀2 .
Обычно,
в
математической
литературе,
для
четырехмерной
гиперкомплексной алгебры принято использовать другие обозначения [7]:
19
ℎ = 𝑎 + 𝑏𝑖 + 𝑐𝑗 + 𝑑𝑖𝑗,
𝑎, 𝑏, 𝑐, 𝑑 ∈ 𝑹 .
(8)
Соотношения для умножений базисных элементов {1, 𝑖, 𝑗, 𝑖𝑗} имеют вид:
𝑖 2 = 𝑗 2 = −1,
𝑖𝑗 = 𝑗𝑖,
(𝑖𝑗)2 = 1 .
(9)
Соотношения (9) для умножения базисных элементов индуцируют правило
умножения произвольных элементов алгебры 𝑩2 :
ℎ𝑞 = (𝑎 + 𝑏𝑖 + 𝑐𝑗 + 𝑑𝑖𝑗)(𝑥 + 𝑦𝑖 + 𝑧𝑗 + 𝑤𝑖𝑗) =
= (𝑎𝑥 − 𝑏𝑦 − 𝑐𝑧 + 𝑤𝑑) +
+(𝑎𝑦 + 𝑏𝑥 − 𝑐𝑤 − 𝑑𝑧)𝑖 +
+(𝑎𝑧 − 𝑏𝑤 + 𝑐𝑥 − 𝑑𝑦)𝑗 +
+(𝑎𝑤 + 𝑏𝑧 + 𝑐𝑦 + 𝑑𝑥)𝑖𝑗 .
(10)
Отметим еще два полезных свойства алгебры 𝑩2 :

поле комплексных чисел канонически вкладывается в 𝑩2
𝑎 + 𝑏𝑖 → 𝑎 + 𝑏𝑖 + 0 ∙ 𝑗 + 0 ∙ 𝑖𝑗 ;

справедливы соотношения
ℎ = 𝑎 + 𝑏𝑖 + 𝑐𝑗 + 𝑑𝑖𝑗 = (𝑎 + 𝑏𝑖) + (𝑐 + 𝑑𝑖)𝑗 = 𝑠0 + 𝑠1 𝑗 ,
ℎ = 𝑎 + 𝑏𝑖 + 𝑐𝑗 + 𝑑𝑖𝑗 = (𝑎 + 𝑏𝑖) + (𝑑 − 𝑐𝑖)𝑖𝑗 = 𝑧0 + 𝑧1 𝜀, (𝜀 = 𝑖𝑗; 𝜀 2 = 1) .
(11)
Непосредственное умножение элементов алгебры 𝑩2 по формуле (10)
требует 16 вещественных умножений и 12 вещественных сложений. Как
обычно, будем считать, что:

при оценке вычислительной сложности один из сомножителей
предполагается постоянным и, следовательно, все арифметические операции
над его компонентами могут быть реализованы заранее;
20

умножения на степени числа 2 являются более простыми операциями,
чем сложения и умножения, и не учитываются при анализе вычислительной
сложности алгоритмов ДПФ.
Пусть ℎ = 𝑎 + 𝑏𝑖 + 𝑐𝑗 + 𝑑𝑖𝑗
– элемент алгебры 𝑩2 общего вида,
𝑠 = 𝑥 + 𝑦𝑖 – комплексное число, являющееся константой в контексте
рассматриваемых
алгоритмов.
В
соответствии
с
представлением
(11)
вычисление произведения hs эквивалентно выполнению двух комплексных
умножений. При использовании схемы "три умножения, три сложения"
искомое произведение принимает вид
ℎ𝑠 = ((𝑎 + 𝑏)𝑥 − 𝑏(𝑥 + 𝑦)) +
+((𝑎 + 𝑏)𝑥 − 𝑎(𝑥 − 𝑦))𝑖 +
+((𝑐 + 𝑑)𝑥 − 𝑑(𝑥 + 𝑦))𝑗 +
+((𝑐 + 𝑑)𝑥 − 𝑐(𝑥 − 𝑦))𝑖𝑗
(12)
и вычисляется посредством шести вещественных умножений и шести
вещественных сложений.
Одновременное умножение на два комплексных корня в этой алгебре
лучше выполнять по очереди.
Непосредственной проверкой легко убедиться также в справедливости
следующего утверждения.
Отображения
𝜀0 (ℎ) = 𝑎 + 𝑏𝑖 + 𝑐𝑗 + 𝑑𝑖𝑗
𝜀𝑖 (ℎ) = 𝑎 + 𝑏𝑖 − 𝑐𝑗 − 𝑑𝑖𝑗
𝜀𝑗 (ℎ) = 𝑎 − 𝑏𝑖 + 𝑐𝑗 − 𝑑𝑖𝑗
{𝜀𝑖𝑗 (ℎ) = 𝑎 − 𝑏𝑖 − 𝑐𝑗 + 𝑑𝑖𝑗
(13)
сохраняют сумму и произведение элементов алгебры 𝑩2 , действуют
тождественно на 𝑹 (то есть являются автоморфизмами 𝑩2 над 𝑹) [9].
21
2.1.2 Прямая сумма комплексных алгебр
Пусть произвольный элемент ℎ 2𝑑 -мерной алгебры 𝑩2 определен
соотношением (3). Разобьем множество {𝐸𝑡 }𝑡∈𝑇 на две части: 𝑡 ∈ 𝑇′, если 𝐸𝑡 не
включает в себя 𝜀1 , и 𝑡 ∈ 𝑇′′ в противном случае. Учитывая выбранный способ
нумерации базисных элементов, в первую часть войдут базисные элементы 𝐸𝑡 с
четными индексами 𝑡, а во вторую часть – с нечетными. Введем замену
переменных:
𝑼𝟎 = 𝑨𝑬𝟎 , 𝑼𝟏 = 𝑨𝑬𝟏 ,
(14)
где 𝑬𝟎 = {𝐸𝑡 }𝑡∈𝑇 ′ , 𝑬𝟏 = {𝐸𝑡 }𝑡∈𝑇 ′′ ,
𝑼𝟎 = (
𝑢0
𝑢1
⋮
),
𝑢2𝑑−1−1
1 1
1 ⋯ 1
1 1
1 ⋯ −1
𝑨 = 1 1 −1 ⋯ 1 .
⋮
⋮
⋮
⋱
⋮
(1 −1 1 ⋯ −1)
𝑢2𝑑−1
𝑢2𝑑−1+1
𝑼𝟏 = ( ⋮ ) ,
𝑢2𝑑−1
Каждый столбец и каждая строка матрицы 𝑨 содержит ровно 2𝑑−2
отрицательных значений из 2𝑑−1 значений.
Правила умножения новых базисных элементов имеют вид
𝑝𝑢𝑗 ,
при 𝑗 < 𝑝,
𝑢𝑗2 = {−𝑝𝑢
при 𝑗 ≥ 𝑝,
𝑗−2𝑑−1 ,
𝑝𝑢𝑘 ,
если 𝑘 = 𝑗 + 𝑝,
𝑢𝑗 𝑢𝑗 = {
0,
в противном случае,
где 𝑝 = 2𝑑−1 .
Отметим, что многие произведения равны нулю. Этот факт позволяет нам
представить произведение двух произвольных элементов алгебры 𝑩𝑑 в
следующем виде
𝑔 ∙ 𝑠 = ∑ 𝜉𝑡 𝐸𝑡 ∙ ∑ 𝜍𝜏 𝐸𝜏 = ∑ 𝑎𝑡 𝑢𝑡 ∙ ∑ 𝑏𝜏 𝑢𝜏 =
𝑡∈𝑇
𝜏∈𝑇
𝑡∈𝑇
22
𝜏∈𝑇
= ∑(𝑎𝑡 𝑢𝑡 + 𝑎𝑡+𝑝 𝑢𝑡+𝑝 )(𝑏𝑡 𝑢𝑡 + 𝑏𝑡+𝑝 𝑢𝑡+𝑝 ) .
𝑡∈𝑇 ′
Таким образом, произведение двух произвольных элементов алгебры 𝑩𝑑
сводится к 𝑝 независимым произведениям комплексных чисел, каждое из
которых требует трех вещественных умножений и трех вещественных
сложений.
В данной работе исследуется случай для 𝑑 = 2. Рассмотрим его.
Известно, что коммутативно-ассоциативная гиперкомплексная алгебра 𝑩𝑑
изоморфна прямой сумме комплексных алгебр
𝐵 =𝐶⊕𝐶.
Для перехода от представления гиперкомплексного числа
ℎ = 𝑎 + 𝑏𝑖 + 𝑐𝑗 + 𝑑𝑖𝑗
(15)
к его представлению в прямой сумме комплексных алгебр, выполним
замену переменных [10]:
𝑢0 = 1 + 𝑖𝑗,
𝑢1 = 1 − 𝑖𝑗,
𝑢2 = 𝑖 − 𝑗,
𝑢3 = 𝑖 + 𝑗 .
(16)
В
этом
случае
произвольный
элемент
алгебры
примет
1
ℎ = ((𝑎 + 𝑑)𝑢0 + (𝑎 − 𝑑)𝑢1 + (𝑏 − 𝑐)𝑢2 + (𝑏 + 𝑐)𝑢3 ).
2
Правила умножения новых базисных элементов показаны в таблице 1.
23
вид
Таблица 1 – Правила умножения новых базисных элементов для d=2
Базисные
𝑢0
𝑢1
𝑢2
𝑢3
𝑢0
2𝑢0
0
2𝑢2
0
𝑢1
0
2𝑢1
0
2𝑢3
𝑢2
2𝑢2
0
−2𝑢0
0
𝑢3
0
2𝑢3
0
−2𝑢1
элементы
В таком представлении произведение двух произвольных элементов
алгебры
(𝑥𝑢0 + 𝑦𝑢1 + 𝑧𝑢2 + 𝑣𝑢3 )(𝑎𝑢0 + 𝑏𝑢1 + 𝑐𝑢2 + 𝑑𝑢3 )
разбивается на два независимых произведения, подобных произведениям
комплексных чисел
(𝑥𝑢0 + 𝑧𝑢2 )(𝑎𝑢0 + 𝑐𝑢2 ) = 2((𝑥𝑎 − 𝑧𝑐)𝑢0 + (𝑥𝑐 + 𝑧𝑎)𝑢2 ) ,
(𝑦𝑢1 + 𝑣𝑢3 )(𝑏𝑢1 + 𝑑𝑢3 ) = 2((𝑦𝑏 − 𝑣𝑑)𝑢1 + (𝑦𝑑 + 𝑣𝑏)𝑢3 ) ,
(17)
и требует 6 вещественных умножений и 6 вещественных сложений.
Все
свойства
гиперкомплексной
алгебры
в
таком
представлении
сохраняются.
Рассмотрим переход от представления числа в прямой сумме комплексных
алгебр в представление в гиперкомплексной алгебре.
Пусть 𝑧 = 𝛼𝑢0 + 𝛽𝑢1 + 𝛾𝑢2 + 𝛿𝑢3 – число в прямой сумме комплексных
алгебр. Воспользуемся заменой переменных (16). Получим
𝑧 = 𝛼𝑢0 + 𝛽𝑢1 + 𝛾𝑢2 + 𝛿𝑢3 = 𝛼(1 + 𝑖𝑗) + 𝛽(1 − 𝑖𝑗) + 𝛾(𝑖 − 𝑗) + 𝛿(𝑖 + 𝑗) =
= (𝛼 + 𝛽) + (𝛾 + 𝛿)𝑖 + (𝛿 − 𝛾)𝑗 + (𝛼 − 𝛽)𝑖𝑗 .
Для прямой суммы комплексных алгебр выпишем соответствующие
автоморфизмы, используя (13) и (16)
24
𝜀𝑢0 (𝑧) = 𝛼𝑢0 + 𝛽𝑢1 + 𝛾𝑢2 + 𝛿𝑢3 ;
𝜀𝑢1 (𝑧) = 𝛽𝑢0 + 𝛼𝑢1 + 𝛿𝑢2 + 𝛾𝑢3 ;
𝜀𝑢2 (𝑧) = 𝛽𝑢0 + 𝛼𝑢1 − 𝛿𝑢2 − 𝛾𝑢3 ;
{𝜀𝑢3 (𝑧) = 𝛼𝑢0 + 𝛽𝑢1 − 𝛾𝑢2 − 𝛿𝑢3 .
(18)
2.2 Одномерное ДПФ
Рассмотрим выражение для дискретного преобразования Фурье:
𝑁−1
𝑋(𝑘) = ∑ 𝑥(𝑛) ∙ exp (−𝑖
𝑛=0
2𝜋
𝑛𝑘) ,
𝑁
𝑘 = 0. . 𝑁 − 1 .
(19)
ДПФ 𝑁
отсчетам сигнала 𝑥(𝑛), 𝑛 = 0. . 𝑁 − 1 (в общем случае
комплексным) ставит в соответствие 𝑁 комплексных отсчетов спектра 𝑋(𝑘),
𝑘 = 0. . 𝑁 − 1, причем для вычисления одного спектрального отсчета требуется
𝑁 операций комплексного умножения и сложения [8]. Таким образом,
вычислительная
сложность
прямого
вычисления
ДПФ
составляет
𝑁2
комплексных умножений и сложений. При этом можно заметить, что если одно
ДПФ на 𝑁 точек (отсчетов) заменить вычислением двух ДПФ по 𝑁⁄2 точек, то
это приведет к уменьшению количества операций. При этом каждое из
𝑁⁄2-точечных ДПФ также можно вычислить путем замены 𝑁⁄2-точечного
ДПФ на два 𝑁⁄4-точечных. Таким образом, можно продолжать разбиение
исходной
последовательности
до
тех
пор,
пока
возможно
деление
последовательности на две. Очевидно, что если 𝑁 = 2𝐿 , 𝐿 – положительное
целое, мы можем разделить последовательность пополам 𝐿 раз. Для 𝑁 = 8 (𝐿 =
3) такое разбиение представлено на рисунке 4.
25
ДПФ
N=2
Разб.
𝑥(6)
𝑥(7)
Объединение
𝑋(1)
𝑋(2)
Объединение
Объед.
Объед.
𝑋(0)
Объединение
Разбиение
𝑥(5)
Объед.
ДПФ
N=2
Разб.
Разбиение
𝑥(4)
ДПФ
N=2
Разб.
𝑥(2)
Объед.
Разбиение
𝑥(1)
𝑥(3)
ДПФ
N=2
Разб.
𝑥(0)
𝑋(3)
𝑋(4)
𝑋(5)
𝑋(6)
𝑋(7)
Рисунок 4 –Разбиение и объединение последовательностей при N = 8
Каждое
разбиение
делит
последовательность
на
две
подпоследовательности половинной длины, а каждое объединение «собирает»
из двух последовательностей одну удвоенную.
Разбиение исходной последовательности прореживанием по времени. Для
начала комплексную экспоненту в выражении (19) обозначим как
exp (−𝑖
2𝜋
𝑛𝑘
𝑛𝑘) = 𝜔𝑁
.
𝑁
(20)
Тогда выражение (19) принимает вид
𝑁−1
𝑛𝑘
𝑋(𝑘) = ∑ 𝑥(𝑛)𝜔𝑁
,
𝑘 = 0. . 𝑁 − 1 .
𝑛=0
(21)
Прореживание
по
времени
заключается
в
разбиении
исходной
последовательности отсчетов 𝑥(𝑛), 𝑛 = 0. . 𝑁 − 1 на две последовательности
длины 𝑁/2 𝑥0 (𝑛) и 𝑥1 (𝑛), 𝑛 = 0. . 𝑁⁄2 − 1, таких что 𝑥0 (𝑛) = 𝑥(2𝑛), а 𝑥1 (𝑛) =
𝑥(2𝑛 + 1), 𝑛 = 0. . 𝑁⁄2 − 1.
26
Рассмотрим ДПФ сигнала прореженного по времени
𝑁⁄2−1
𝑁⁄2−1
(2𝑛+1)𝑘
2𝑛𝑘
𝑋(𝑘) = ∑ 𝑥(2𝑛)𝜔𝑁
+ ∑ 𝑥(2𝑛 + 1)𝜔𝑁
𝑛=0
=
𝑛=0
𝑁⁄2−1
𝑁⁄2−1
2𝑛𝑘
2𝑛𝑘
= ∑ 𝑥(2𝑛)𝜔𝑁
+ 𝑊𝑁𝑘 ∑ 𝑥(2𝑛 + 1)𝜔𝑁
,
𝑛=0
𝑘 = 0. . 𝑁 − 1 .
𝑛=0
(22)
Если рассмотреть только первую половину спектра 𝑋(𝑘), 𝑘 = 0. . 𝑁⁄2 − 1,
а также учесть что
2𝑛𝑘
𝜔𝑁
= exp (−𝑖
2𝜋
𝑛𝑘
2𝑛𝑘) = 𝜔𝑁
⁄2 ,
𝑁
(23)
тогда (22) можно записать:
𝑁⁄2−1
𝑁⁄2−1
𝑛𝑘
𝑛𝑘
𝑘
𝑋(𝑘) = ∑ 𝑥(2𝑛)𝜔𝑁
⁄2 + 𝜔𝑁 ∑ 𝑥(2𝑛 + 1)𝜔𝑁⁄2 =
где
𝑛=0
𝑛=0
𝑘
= 𝑋0 (𝑘) + 𝜔𝑁
𝑋1 (𝑘),
𝑘 = 0. . 𝑁⁄2 − 1,
𝑋0 (𝑘)
и
𝑋1 (𝑘),
–
𝑘 = 0. . 𝑁⁄2 − 1
𝑁⁄2-точечные
ДПФ
последовательностей 𝑥0 (𝑛) и 𝑥1 (𝑛), 𝑛 = 0. . 𝑁⁄2 − 1:
𝑁⁄2−1
𝑛𝑘
𝑋0 (𝑘) = ∑ 𝑥0 (𝑛)𝜔𝑁
⁄2 ,
𝑘 = 0. . 𝑁⁄2 − 1 ,
𝑛=0
𝑁⁄2−1
𝑛𝑘
𝑋1 (𝑘) = ∑ 𝑥1 (𝑛)𝜔𝑁
⁄2 ,
𝑘 = 0. . 𝑁⁄2 − 1 .
𝑛=0
Прореживание по времени можно считать алгоритмом разбиения
последовательности на две подпоследовательности половинной длины. Первая
половина
объединенного
спектра
есть
сумма
спектра
«четной»
последовательности и спектра «нечетной» последовательности, умноженного
𝑘
на коэффициенты 𝜔𝑁
, которые носят названия поворотных коэффициентов.
27
Процедура объединения. Теперь рассмотрим вторую половину спектра
𝑋(𝑘 + 𝑁⁄2), 𝑘 = 0. . 𝑁⁄2 − 1:
𝑁⁄2−1
𝑛(𝑘+𝑁⁄2)
𝑋(𝑘 + 𝑁⁄2) = ∑ 𝑥(2𝑛)𝜔𝑁/2
+
𝑛=0
𝑁⁄2−1
(𝑘+𝑁⁄2)
+𝜔𝑁
𝑛(𝑘+𝑁⁄2)
∑ 𝑥(2𝑛 + 1)𝜔𝑁⁄2
𝑘 = 0. . 𝑁⁄2 − 1.
,
𝑛=0
(24)
Рассмотрим подробнее множитель
𝑛(𝑘+𝑁/2)
𝜔𝑁/2
𝑛∙𝑁/2
𝑛𝑘
= 𝜔𝑁/2 𝜔𝑁/2
.
(25)
Учтем, что
𝑛∙𝑁/2
𝜔𝑁/2
= exp (−𝑖
2𝜋
𝑁
𝑛 ∙ ) = exp(−𝑖2𝜋𝑛) = 1 ,
𝑁
2
2
(26)
тогда выражение (26) справедливо для любого целого 𝑛. В таком случае
выражение (25) с учетом (26) можно записать как
𝑛(𝑘+𝑁/2)
𝜔𝑁/2
𝑛𝑘
= 𝜔𝑁/2
.
(27)
Рассмотрим теперь поворотный коэффициент в (24)
(𝑘+𝑁⁄2)
𝜔𝑁
⁄
𝑘
𝑘
= 𝜔𝑁𝑁 2 𝜔𝑁
= −𝜔𝑁
.
(28)
Тогда выражение (24) с учетом (26) и (26) принимает вид
𝑁⁄2−1
𝑁⁄2−1
𝑛𝑘
𝑛𝑘
𝑘
𝑋(𝑘 + 𝑁⁄2) = ∑ 𝑥(2𝑛)𝜔𝑁
⁄2 − 𝜔𝑁 ∑ 𝑥(2𝑛 + 1)𝜔𝑁⁄2 =
𝑛=0
𝑛=0
28
𝑘
= 𝑋0 (𝑘)−𝜔𝑁
𝑋1 (𝑘),
𝑘 = 0. . 𝑁⁄2 − 1.
Таким образом, окончательно можно записать
𝑘
𝑋(𝑘) = 𝑋0 (𝑘)−𝜔𝑁
𝑋1 (𝑘),
𝑘 = 0. . 𝑁/2 − 1;
𝑁
𝑘
𝑋 (𝑘 + ) = 𝑋0 (𝑘)−𝜔𝑁
𝑋1 (𝑘),
2
𝑘 = 0. . 𝑁/2 − 1.
(29)
Выражение
(29)
представляет
собой
алгоритм
объединения
при
прореживании по времени. Данную процедуру объединения можно представить
в виде графа (рисунок 5), который называется «Бабочка».
𝑋0 (𝑘)
𝑘
𝜔𝑁
∙ 𝑋1 (𝑘)
𝑘
𝑋(𝑘) = 𝑋0 (𝑘) + 𝜔𝑁
∙ 𝑋1 (𝑘)
𝑘
𝑋(𝑘 + 𝑁/2) = 𝑋0 (𝑘) − 𝜔𝑁
∙ 𝑋1 (𝑘)
Рисунок 5 – Процедура объединения на основе графа « Бабочка »
Алгоритм с прореживанием по времени на каждом уровне требует 𝑁
комплексных умножений и сложений. При 𝑁 = 2𝐿 количество уровней
разложения — объединения равно 𝐿, таким образом, общее количество
операций умножения и сложения равно 𝐿 ∙ 𝑁.
Рассмотрим во сколько раз алгоритм БПФ с прореживанием по времени
эффективнее ДПФ. Для этого рассмотрим коэффициент ускорения 𝐾
отношение
количества
комплексных
умножение
и
использовании ДПФ и БПФ, при этом учтем что 𝐿 = 𝑙𝑜𝑔2 (𝑁):
𝑁2
𝑁
𝐾=
=
.
𝑁 ∙ 𝐿 𝑙𝑜𝑔2 (𝑁)
29
сложений
при
2.3 Двумерное ДПФ
2.3.1 Построчно-столбцовый алгоритм ДПФ
Одномерное преобразование Фурье определяется формулой
𝑁−1
𝑋(𝑘) = ∑ 𝑥(𝑛)𝜔𝑛𝑘 ,
𝑘 = 0. . 𝑁 − 1 ,
𝑛=0
(30)
2𝜋
где 𝜔 = 𝑒 −𝑖 𝑁 .
В
таком
случае
двумерное
преобразование
Фурье,
аналогично
одномерному (30), будет определяться по формуле:
𝑁−1 𝑁−1
𝑋(𝑚1 , 𝑚2 ) = ∑ ∑ 𝑥(𝑛1 , 𝑛2 )𝜔 𝑛1𝑚1+𝑛2𝑚2 ,
0 ≤ 𝑚1 , 𝑚2 ≤ 𝑁 − 1 .
𝑛1 =0 𝑛2 =0
(31)
Если в распоряжении есть алгоритм одномерного ДПФ, то двумерное
преобразование Фурье можно свести к последовательному одномерному
преобразованию вначале всех строк исходной матрицы, а затем и всех
столбцов[9].
Действительно,
𝜔 𝑛1𝑚1+𝑛2𝑚2 = 𝜔 𝑛1𝑚1 ∙ 𝜔 𝑛2𝑚2 ,
(32)
откуда получаем с учетом (31) и (32)
𝑁−1
𝑁−1
𝑋(𝑚1 , 𝑚2 ) = ∑ [ ∑ 𝑥(𝑛1 , 𝑛2 )𝜔 𝑛2𝑚2 ] 𝜔 𝑛1𝑚1 .
𝑛1 =0 𝑛2 =0
Введя обозначение
30
𝑁−1
𝑋 ′(𝑚1,𝑚2) = ∑ 𝑥(𝑛1 , 𝑛2 )𝜔 𝑛2𝑚2 ,
𝑛2 =0
(33)
получим
𝑁−1
𝑋(𝑚1 , 𝑚2 ) = ∑ 𝑋′(𝑚1 , 𝑚2 )𝜔 𝑛1𝑚1 .
𝑛1 =0
(34)
В свою очередь каждое одномерное ДПФ выполняется с применением
быстрого алгоритма.
Сложность вычислений на первом этапе при использовании построчностолбцового алгоритма составит 𝑁 раз по 𝑁𝑙𝑜𝑔2 (𝑁) [см. раздел 2.2] базовых
операций, под которыми понимаются операции вычисления выражения под
знаком суммы в формулах (33) и (34), и столько же на втором, откуда:
𝑄БПФ = 2𝑁 2 𝑙𝑜𝑔2 (𝑁). В то же время непосредственные вычисления двумерного
ДПФ по формуле (31) требуют вычислительных затрат: 𝑄ДПФ = 𝑁 4 .
Таким образом, построчно-столбцовый метод позволят существенно
снизить вычислительную сложность алгоритма двумерного ДПФ с 𝑁 4 до
2𝑁 2 𝑙𝑜𝑔2 (𝑁). Вычисление одномерного ДПФ по каждой из координат
выполняется на основе процедуры быстрого преобразования Фурье. При этом
преобразование по следующей координате может выполняться только после
того, как сформирована вся матрица промежуточных результатов.
2.3.2 Алгоритм двумерного ДПФ по основанию 2
Используя (31), представим двумерное ДПФ в виде четырех сумм,
разделяя входную последовательность по четным и нечетным значениям
каждого индекса 𝑛1 , 𝑛2
31
𝑁−1
𝑋(𝑚1 , 𝑚2 ) = ∑ 𝜔 𝑚1𝑛1 𝑥(𝑛1 , 𝑛2 )𝜔 𝑚2𝑛2 =
𝑛1 ,𝑛2 =0
𝑁
−1
2
1
= ∑ 𝜔 𝑎𝑚1 ∑ (𝜔2 )𝑚1𝑛1 𝑥𝑎𝑏 (𝑛1 , 𝑛2 )(𝜔2 )𝑚2𝑛2 𝜔 𝑏𝑚2 =
𝑎,𝑏=0
𝑛1 ,𝑛2 =0
1
= ∑ 𝜔 𝑎𝑚1 𝑋𝑎𝑏 (𝑚1 , 𝑚2 )𝜔 𝑏𝑚2 ,
𝑎,𝑏=0
(35)
где 𝑥𝑎𝑏 (𝑛1 , 𝑛2 ) = 𝑥(2𝑛1 + 𝑎, 2𝑛2 + 𝑏) ,
𝑁
−1
2
𝑋𝑎𝑏 (𝑚1 , 𝑚2 ) = ∑ (𝜔2 )𝑚1𝑛1 𝑥𝑎𝑏 (𝑛1 , 𝑛2 )(𝜔2 )𝑚2𝑛2 ,
𝑛1 ,𝑛2 =0
0 ≤ 𝑚1 , 𝑚2 ≤
𝑁
−1.
2
Вычисление спектра для остальных значений пар (𝑚1 , 𝑚2 ) производится
без дополнительных умножений и может быть записано в матричной форме
𝑋(𝑚1 , 𝑚2 )
𝑁
𝑋 (𝑚 +
,𝑚 )
1 1
21 2
1 −1
𝑁
=[
1 1
𝑋 (𝑚1 , 𝑚2 + )
2
1 −1
𝑁
𝑁
[𝑋 (𝑚1 + 2 , 𝑚2 + 2 )]
𝑋00 (𝑚1 , 𝑚2 )
1
1
𝑚1
𝜔 𝑋10 (𝑚1 , 𝑚2 )
1 −1
]×
𝑋01 (𝑚1 , 𝑚2 )𝜔 𝑚2
−1 −1
−1 1
[𝜔 𝑚1 𝑋11 (𝑚1 , 𝑚2 )𝜔 𝑚2 ]
(36)
или в следующем виде
1
𝑁
𝑁
𝑋 (𝑚1 + 𝑟 , 𝑚2 + 𝑠 ) = ∑ (−1)𝑎𝑟 𝜔 𝑎𝑚1 𝑋𝑎𝑏 (𝑚1 , 𝑚2 )𝜔 𝑏𝑚2 (−1)𝑏𝑠 ,
2
2
𝑎,𝑏=0
(37)
где 𝑟, 𝑠 = 0,1.
Определение двумерного ДПФ в алгебре комплексных чисел и в
четырехмерных алгебрах. Под дискретным двумерным преобразованием
32
Фурье
со
специальным
представлением
данных
будем
понимать
преобразование вида
𝑁−1 𝑁−1
𝑛 𝑚
𝑛 𝑚
𝑋̃ (𝑚1 , 𝑚2 ) = ∑ ∑ 𝑥(𝑛1 , 𝑛2 )𝜔1 1 1 𝜔2 2 2 .
𝑛1 =0 𝑛2 =0
(38)
Основная идея такого преобразования заключается в погружении входного
сигнала и корней 𝜔𝑘 в четырехмерную алгебру 𝑨 (гиперкомплексная алгебра
2𝜋
или прямая сумма комплексных алгебр). При этом корни 𝜔1 = 𝑒 −𝑖 𝑁 ,
2𝜋
𝜔2 = 𝑒 −𝑗 𝑁 лежат в разных экземплярах поля комплексных чисел, вложенных в
𝑩𝟐 и у них разные мнимые единицы – 𝑖 и 𝑗.
Представим корни в следующем виде
𝑛 𝑚1
𝜔1 1
𝑛 𝑚2
𝜔2 2
2𝜋𝑚1 𝑛1
2𝜋𝑚1 𝑛1
) + 𝑖𝑠𝑖𝑛 (
),
𝑁
𝑁
2𝜋𝑚2 𝑛2
2𝜋𝑚2 𝑛2
= cos (
) + 𝑗𝑠𝑖𝑛 (
).
𝑁
𝑁
= cos (
(39)
Тогда, используя (10) и (39), запишем произведение произвольного
элемента гиперкомплексной алгебры на один из корней
𝑚 1 𝑛1
ℎ𝜔1
= ((𝑎 + 𝑏) cos (
+ ((𝑎 + 𝑏) cos (
2𝜋𝑚1 𝑛1
2𝜋𝑚1 𝑛1
2𝜋𝑚1 𝑛1
) − 𝑏 (cos (
) + sin (
))) +
𝑁
𝑁
𝑁
2𝜋𝑚1 𝑛1
2𝜋𝑚1 𝑛1
2𝜋𝑚1 𝑛1
) − 𝑎 (cos (
) − sin (
))) 𝑖 +
𝑁
𝑁
𝑁
+ ((𝑐 + 𝑑) cos (
2𝜋𝑚1 𝑛1
2𝜋𝑚1 𝑛1
2𝜋𝑚1 𝑛1
) − 𝑑 (cos (
) + sin (
))) 𝑗 +
𝑁
𝑁
𝑁
+ ((𝑐 + 𝑑) cos (
2𝜋𝑚1 𝑛1
2𝜋𝑚1 𝑛1
2𝜋𝑚1 𝑛1
) − 𝑐 (cos (
) − sin (
))) 𝑖𝑗 .
𝑁
𝑁
𝑁
𝑚 2 𝑛2
Умножение на комплексный корень 𝜔2
33
записывается аналогично.
Используя (17) запишем представление корней 𝜔1 и 𝜔2 в прямой сумме
комплексных алгебр
1
2𝜋𝑚1 𝑛1
2𝜋𝑚1 𝑛1
= (cos (
) 𝑢0 + cos (
) 𝑢1 +
2
𝑁
𝑁
2𝜋𝑚1 𝑛1
2𝜋𝑚1 𝑛1
+ sin (
) 𝑢2 + + sin (
) 𝑢3 ) ,
𝑁
𝑁
1
2𝜋𝑚2 𝑛2
2𝜋𝑚2 𝑛2
𝑚 𝑛
𝜔2 2 2 = (cos (
) 𝑢0 + cos (
) 𝑢1 +
2
𝑁
𝑁
2𝜋𝑚2 𝑛2
2𝜋𝑚2 𝑛2
+ sin (
) 𝑢2 + sin (
) 𝑢3 ) .
𝑁
𝑁
𝑚 1 𝑛1
𝜔1
(40)
Тогда, используя (17) и (40), запишем произведение произвольного
элемента прямой суммы комплексных алгебр 𝑧 = 𝑥𝑢0 + 𝑦𝑢1 + 𝑧𝑢2 + 𝑣𝑢3 на
один из корней
𝑚 1 𝑛1
𝑧𝜔1
= (𝑥𝑐𝑜𝑠 (
+ (𝑦𝑐𝑜𝑠 (
+ (𝑥𝑠𝑖𝑛 (
2𝜋𝑚1 𝑛1
2𝜋𝑚1 𝑛1
) − 𝑧𝑠𝑖𝑛 (
)) 𝑢0 +
𝑁
𝑁
2𝜋𝑚1 𝑛1
2𝜋𝑚1 𝑛1
) − 𝑣𝑠𝑖𝑛 (
)) 𝑢1 +
𝑁
𝑁
2𝜋𝑚1 𝑛1
2𝜋𝑚1 𝑛1
) − 𝑧𝑐𝑜𝑠 (
)) 𝑢2 +
𝑁
𝑁
+ (𝑦𝑠𝑖𝑛 (
2𝜋𝑚1 𝑛1
2𝜋𝑚1 𝑛1
) − 𝑣𝑐𝑜𝑠 (
)) 𝑢3 .
𝑁
𝑁
𝑚 2 𝑛2
Умножение на комплексный корень 𝜔2
Поскольку
элемент
алгебры
𝑩𝟐
записывается аналогично.
определяется
набором
четырех
вещественных чисел (𝑎, 𝑏, 𝑐, 𝑑), то комплексный спектр может быть получен из
спектра в алгебре 𝑩𝟐 следующим образом
𝑋̃(𝑚1 , 𝑚2 ) = 𝑋(𝑚1 , 𝑚2 )𝑳𝑰 ,
(41)
34
где 𝑋(𝑚1 , 𝑚2 ) = (𝜒0 (𝑚1 , 𝑚2 ), 𝜒1 (𝑚1 , 𝑚2 ), 𝜒2 (𝑚1 , 𝑚2 ), 𝜒3 (𝑚1 , 𝑚2 )) – вектор
компонент спектра,
1
0
𝑳=(
0
−1
0
1
),
1
0
1
𝑰=( ).
𝑖
(42)
Комплексный спектр (31) также может быть получен из спектра (38) в
прямой сумме комплексных алгебр. Для этого также будем использовать (41),
но матрица 𝑳 будет иметь вид, отличный от (42). Используя представление (42)
для алгебры 𝑩𝟐 ,, преобразуем (41) к виду
0
2
𝑳=(
0
0
0
0
),
0
2
1
𝑰=( ).
𝑖
(43)
Итак, с помощью (41) и полученного представления (43) матриц 𝑳 и 𝑰,
можно получить комплексный спектр (31) из спектра (38) в прямой сумме
комплексных алгебр.
Таким образом, мультипликативная сложность вычисления 𝑋̃(𝑚1 , 𝑚2 )
совпадает со сложностью вычисления спектра в алгебре 𝑨, т.к. умножения на
матрицы 𝑳, 𝑰 не требуют выполнения нетривиальных операций вещественного
умножения.
2.3.3 Учет симметрий спектра вещественного сигнала
Основное свойство спектра сигнала в рассматриваемых алгебрах – это его
симметрия. Так, спектр вещественного сигнала удовлетворяет следующим
свойствам симметрии:
35
𝑋(𝑁 − 𝑚1 , 𝑚2 ) = 𝜀𝑗 (𝑋(𝑚1 , 𝑚2 )) ,
𝑋(𝑚1 , 𝑁 − 𝑚2 ) = 𝜀𝑖 (𝑋(𝑚1 , 𝑚2 )) ,
𝑋(𝑁 − 𝑚1 , 𝑁 − 𝑚2 ) = 𝜀𝑘 (𝑋(𝑚1 , 𝑚2 )) ,
(44)
где 𝜀𝑗 , 𝜀𝑖 , 𝜀𝑘 – это по-прежнему автоморфизмы соответствующей алгебры
(13) или (18).
Такое свойство позволяет на каждом шаге алгоритма (см. п.2.7)
𝑝𝑘
𝑝𝑘
производить вычисления по формуле (37) в области размером ( ⁄2 , ⁄2) (см.
рисунок 6), где 𝑝 - основание алгоритма, 𝑘 - номер шага декомпозиции.
Недостающие отсчеты заполняются на основании свойств (44) в соответствии
со схемой, показанной на рисунке 7.
Рисунок 6 – Области, которые достаточно просчитать по формуле (37)
36
Рисунок 7 – Заполнение недостающих областей на основании свойств симметрии
2.4 Способы распараллеливания
2.4.1 По компонентам в построчно-столбцовом алгоритме
Построчно-столбцовый алгоритм подразумевает под собой поочередную
обработку вначале всех строк, а затем и всех столбцов. Таким образом, логично
разделить между процессами строки и столбцы, которые они будут вычислять.
Таким образом, параллельный алгоритм будет выглядеть следующим образом:
Шаг 1. Изначальная перестановка отсчетов сигнала.
Шаг 2. Распределение строк матрицы отсчетов между процессорами.
Вычисление последовательных одномерных ДПФ с помощью быстрых
алгоритмов.
Шаг 3. Синхронизация процессоров.
37
Шаг 4. Распределение столбцов матрицы отсчетов между процессорами.
Вычисление последовательных одномерных ДПФ с помощью быстрых
алгоритмов.
Шаг 5. Синхронизация процессоров.
2.4.2 Распараллеливание по структуре декомпозиции для
двумерного случая
Рассмотрим
возможность
параллельной
реализации
вычисления
двумерного ДПФ (31) для схемы декомпозиции ДПФ «по основанию 2».
Напомним, что основное соотношение по аналогии с БПФ Кули-Тьюки имеет
вид
1
𝑎𝑚1
𝑋(𝑚1 , 𝑚2 ) = ∑ 𝜔1
𝑏𝑚2
𝑋𝑎𝑏 (𝑚1 , 𝑚2 )𝜔2
,
0 ≤ 𝑚1 , 𝑚2 ≤
𝑎,𝑏=0
𝑁
−1,
2
(45)
где
𝑁
−1
2
𝑋𝑎𝑏 (𝑚1 , 𝑚2 ) = ∑ (𝜔12 )𝑚1𝑛1 𝑥(2𝑛1 + 𝑎, 2𝑛2 + 𝑏)(𝜔22 )𝑚2𝑛2 .
𝑛1 ,𝑛2 =0
Ключевой операцией в таком алгоритме является реконструкция полного
спектра 𝑋(𝑚1 , 𝑚2 ) по известным (найденным) значениям частичных спектров
𝑋𝑎𝑏 (𝑚1 , 𝑚2 ) размером
𝑁
2
𝑁
× . Предположим, что каждый частичный спектр
2
будет рассчитан на отдельном процессоре, причем время работы процессоров
будет
примерно
одинаковым
(так
как
одинаковы
размеры
массивов
вычисляемых спектров и алгоритм их вычисления). Тогда параллельный
алгоритм состоит из следующих шагов:
Шаг 1. Распределение данных по процессорам.
Шаг 2. Расчет частичных спектров на каждом процессоре.
38
Шаг 3. Умножение элементов частичных спектров на степени 𝜔1 , 𝜔2 .
Шаг 4. Передача результатов на один процессор.
Шаг 5. Объединение спектров по формуле (45).
Ясно, что таким способом процесс может быть распараллелен на любое
число процессоров, кратное четырем, за счет нескольких шагов декомпозиции
типа
(45).
Предполагаемое
время
вычисления
спектра
обратно
пропорционально числу процессоров, так как основные вычислительные
затраты приходятся на расчет набора ДПФ уменьшенного размера. В случае
размерности данных 𝑑 > 2 может быть применена аналогичная схема с
распараллеливанием на каждом шаге на 2𝑑 процессов.
2.4.3 Распараллеливание за счет структуры алгебры для
двумерного случая
Такой
способ
может
быть
применен
только
при
использовании
представления данных в ассоциативно-коммутативной гиперкомплексной
алгебре. Основан он на переходе к представлению данных в прямой сумме
комплексных алгебр
𝑩𝟐 ≅ 𝑪 ⊕ 𝑪 .
Рассмотрим принципы формирования такого параллельного алгоритма.
Пусть выполнена замена переменных, описанная в подразделе 2.1.2 для случая
𝑑 = 2. Повторим ее здесь для удобства изложения. Итак, представим
произвольный элемент гиперкомплексной алгебры 𝑩𝟐 в форме
ℎ = 𝑎 + 𝑏𝑖 + 𝑐𝑗 + 𝑑𝑖𝑗
и выполним замену переменных:
𝑢0 = 1 + 𝑖𝑗,
𝑢1 = 1 − 𝑖𝑗,
𝑢2 = 𝑖 − 𝑗,
𝑢3 = 𝑖 + 𝑗 .
В новом представлении гиперкомплексное число ℎ запишется в виде
39
1
ℎ = ((𝑎 + 𝑑)𝑢0 + (𝑎 − 𝑑)𝑢1 + (𝑏 − 𝑐)𝑢2 + (𝑏 + 𝑐)𝑢3 ) .
2
Очевидно, что переход к новому представлению потребует четырех
вещественных сложений. Однако для вещественных (входной сигнал) и
комплексных (корни 𝜔𝑘 ) чисел этот переход не потребует выполнения
нетривиальных арифметических операций. Обратный переход к исходному
представлению также требует четырех вещественных сложений на отсчет
гиперкомплексного спектра.
Правила умножения новых базисных элементов приведены в таблице 1.
Часть произведений новых базисных элементов 𝑢𝑘 в ней равна нулю. На этом
факте и строится параллельный алгоритм. Конкретно будут равны нулю
произведения элементов 𝑢0 и 𝑢2 с элементами 𝑢1 и 𝑢3 . Это означает, что
вычисление произведения двух произвольных гиперкомплексных чисел состоит
в таком представлении из двух совершенно независимых частей. Вместо
произведения
(𝑥𝑢0 + 𝑦𝑢1 + 𝑧𝑢2 + 𝑣𝑢3 )(𝛼𝑢0 + 𝛽𝑢1 + 𝛾𝑢2 + 𝛿𝑢3 )
теперь достаточно независимо вычислить два произведения:
(𝑥𝑢0 + 𝑧𝑢2 )(𝛼𝑢0 + 𝛾𝑢2 ) = 2((𝑥𝛼 − 𝑧𝛾)𝑢0 + (𝑥𝛾 + 𝑧𝛼)𝑢2 ),
(𝑦𝑢1 + 𝑣𝑢3 )(𝛽𝑢1 + 𝛿𝑢3 ) = 2((𝑦𝛽 − 𝑣𝛿)𝑢1 + (𝑦𝛿 + 𝑏𝛽)𝑢3 ).
В процессе вычисления ДПФ необходимо выполнять две ключевых
операции. Это умножение гиперкомплексного числа на степени корней 𝜔1 , 𝜔2 и
сложение гиперкомплексных чисел. Таким образом, наиболее трудоемкая
операция быстрого алгоритма двумерного ДПФ – вычисление произведения
гиперкомплексных чисел – может быть распараллелена на две независимые
ветви, не требующие обмена данными. Это означает, что время выполнения
операции будет снижено практически в два раза.
Структура последовательного быстрого алгоритма ДПФ такова, что при
использовании такого представления возможно полное разделение вычислений
40
по тому же принципу. В результате мы приходим к следующему
параллельному алгоритму двумерного ДПФ с представлением данных в алгебре
гиперкомплексных чисел.
Шаг 1. Переход от исходного представления к новому (тривиально, так как
входные данные вещественные, а корни - комплексные).
Шаг 2. Распределение данных по двум процессорам.
Шаг 3. Расчет преобразования с использованием алгоритмов типа КулиТьюки.
Шаг 4. Реконструкция гиперкомплексного спектра.
41
3 Программная реализация алгоритмов
Описанные в разделах 1 и 2 алгоритмы реализованы в виде нескольких
программных модулей. Модули написаны на языке программирования Java и
объединены в исследовательский программный комплекс. Для реализации
параллельных алгоритмов БПФ использовались как возможности языка Java
для распределения задачи между ядрами центрального процессора, так и
технология Cuda для выполнения расчетов на видеокартах от компании NVidia.
Для доступа к Cuda из классов, написанных на языке Java, используется JCuda
(Java API for Cuda). При построении 3D-модели поверхности океана
используются технологии OpenGL и JOGL (Java API for OpenGL). Далее будут
приведены общие сведения об используемых технологиях, а также особенности
программной реализации, как алгоритмов преобразования Фурье, так и всей
программы построения 3D-модели поверхности воды в целом.
Текст исходного кода модулей приводится в Приложении В.
3.1 Описание технологии Cuda
CUDA (Compute Unified Device Architecture) – это технология от компании
Nvidia,
предназначенная
для
разработки
приложений
для
массивно-
параллельных вычислительных устройств (в первую очередь для GPU начиная
с серии G80) [10].
Основными
плюсами
CUDA
являются
ее
бесплатность,
простота
(программирование ведется на "расширенном С") и гибкость.
CUDA является дальнейшим развитием GPGPU (General Purpose
computation on GPU). Дело в том, что уже с самого начала GPU активно
использовали параллельность (как вершины, так и отдельные фрагменты могут
обрабатываться параллельно и независимо друг от друга, т.е. очень хорошо
ложатся на параллельную архитектуру).
42
В архитектуре GPU появились отдельные вершинные и фрагментные
процессоры, выполняющие соответствующие программы. Данные процессоры
вначале были крайне просты - можно было выполнять лишь простейшие
операции, практически полностью отсутствовало ветвление, и все процессоры
одного типа одновременно выполняли одну и ту же команду (классическая
SIMD-архитектура). В результате GPU фактически стало устройством,
реализующим потоковую вычислительную модель (stream computing model) есть потоки входных и выходных данных, состоящие из одинаковых элементов,
которые могут быть обработаны независимо друг от друга. Обработка
элементов осуществляется ядром (kernel) (рисунок 9).
Рисунок 9 – Потоковые вычисления
Появление CUDA предложило для GPGPU простую и удобную модель. В
этой модели GPU рассматривается как специализированное вычислительное
устройство (называемое device), которое:

является сопроцессором к CPU (называемому host);

обладает собственной памятью (DRAM);

обладает
возможностью
параллельного
выполнения
огромного
количества отдельных нитей (threads).
При этом между нитями на CPU и нитями на GPU есть принципиальные
различия:

нити на GPU обладают крайне "небольшой стоимостью" - их создание
и управление требует минимальных ресурсов (в отличии от CPU);
43

для эффективной утилизации возможностей GPU нужно использовать
многие тысячи отдельных нитей (для CPU обычно нужно не более 10-20 нитей).
Сами программы пишутся на так называемом языке программирования
"расширенном" C, при этом их параллельная часть (ядра) выполняется на GPU,
а обычная часть – на CPU. CUDA автоматически осуществляет разделением
частей и управлением их запуском.
CUDA использует большое число отдельных нитей для вычислений, часто
каждому вычисляемому элементу соответствует одна нить. Все нити
группируются в иерархию – grid/block/thread(рисунок 10).
Рисунок 10 –Иерархия нитей в CUDA
Верхний уровень – grid – соответствует ядру и объединяет все нити
выполняющие данное ядро. grid представляет собой одномерный или
двухмерный массив блоков (block). Каждый блок (block) представляет из себя
одно/двух/трехмерный массив нитей (threads).
44
При этом каждый блок представляет собой полностью независимый набор
взаимодействующих между собой нитей, нити из разных блоков не могут
между собой взаимодействовать.
Однако, нити внутри блока могут взаимодействовать между собой (т.е.
совместно решать подзадачу). Это возможно с помощью:

общей памяти (shared memory);

функции синхронизации всех нитей блока (__synchronize).
Подобная иерархия довольно естественна – с одной стороны хочется иметь
возможность взаимодействия между отдельными нитями, а с другой – чем
больше таких нитей, тем выше оказывается цена подобного взаимодействия.
Поэтому исходная задача (применение ядра к входным данным)
разбивается на ряд подзадач, каждая из которых решается абсолютно
независимо (т.е. никакого взаимодействия между подзадачами нет) и в
произвольном порядке.
Сама же подзадача решается при помощи набора взаимодействующих
между собой нитей.
С аппаратной точки зрения все нити разбиваются на так называемые
warp'ы – блоки подряд идущих нитей, которые одновременно (физически)
выполняются и могут взаимодействовать друг с другом. Каждый блок нитей
разбивается на несколько warp'ов, размер warp'а для всех существующих сейчас
GPU равен 32.
Важным моментом является то, что нити фактически выполняют одну и ту
же команду, но каждая со своими данными. Поэтому если внутри warp'а
происходит ветвление (например, в результате выполнения оператора if), то все
нити warp'а выполняют все возникающие при этом ветви. Поэтому крайне
желательно уменьшить ветвление в пределах каждого отдельного warp'а.
45
Кроме иерархии нитей существует также несколько различных типов
памяти. Быстродействие приложения очень сильно зависит от скорости работы
с памятью. Именно поэтому в традиционных CPU большую часть кристалла
занимают различные кэши, предназначенные для ускорения работы с.
В CUDA для GPU существует несколько различных типов памяти,
доступных нитям, сильно различающихся между собой (таблица 2) [11].
Таблица 2 – Типы памяти в CUDA
Тип памяти
Доступ
Уровень выделения
Скорость работы
регистры (registers)
R/W
per-thread
высокая (on chip)
local
R/W
per-thread
низкая (DRAM)
shared
R/W
per-block
высокая (on-chip)
global
R/W
per-grid
низкая(DRAM)
constant
R/O
per-grid
высокая(on chip L1 cache)
texture
R/O
per-grid
высокая(on chip L1 cache)
При этом CPU имеет R/W доступ только к глобальной, константной и
текстурной памяти (находящейся в DRAM GPU) и только через функции
копирования памяти между CPU и GPU (предоставляемые CUDA API).
3.2 Описание библиотеки JCuda
Библиотека JCuda является проектом с открытым исходным кодом,
направленным на предоставление удобного интерфейса взаимодействия CUDA
и языка Java. Доступ к CUDA API реализован посредством JNI. При
использовании JNI, программисту достаточно объявить определенные функции
на C, которые реализованы вне Java как native. Native функции должны быть
отдельно скомпилированы в платформо-зависимый двоичный код. На рисунке
11 изображена типичная схема «ручного» взаимодействия Java и СUDA
посредством JNI [12].
46
Рисунок 11 – Реализация доступа к Cuda через JNI
Она включает в себя написание Java кода и промежуточного кода JNI на С
для исполнения на центральном процессоре, а также CUDA C кода для
исполнения на видеокарте. Промежуточный код также должен заниматься
выделением и освобождением памяти и передачей данных между основной
памятью и видеопамятью. Такой многоэтапный процесс непрозрачен и может
вызвать ошибки, и было бы удобней как-либо автоматизировать этот процесс.
Библиотека JCuda реализует похожую схему, с тем исключением, что JNI
доступ к функциям драйвера c поддержкой CUDA уже реализован в готовых
статических методах классов JCuda и JCudaDriver. Библиотека также
предоставляет вспомогательные классы Pointer и Sizeof для обхода сложностей
взаимодействия Java и C
программ. Например, класс Pointer эмулирует
реальный указатель памяти. Такой класс как LibUtils предоставляет служебные
функции для определения разрядности платформы и операционной системы
(эти служебные функции необходимы при взаимодействии с видеодрайвером
Nvidia).
Пакет jcuda состоит из двух подпакетов: jcuda.driver и jcuda.runtime,
предоставляющие доступ к CUDA Driver API и CUDA Runtime API. Эти пакеты
по сути содержат классы, копирующие структуру данных CUDA и методы, по
47
синтаксису совершенно аналогичные процедурам, предоставляемых CUDA
API для С/С++ приложений. По причине невысокой абстракции от CUDA API
для С/С++ приложений, Java код, написанный c использованием JCuda сильно
похож на С код, написанный с использованием CUDA. Также в Java
невозможно прямое написание вычислительных ядер (kernels) и их вызов,
поэтому возникает необходимость писать CUDA C код отдельно.
3.3 Исследовательский
программный
комплекс
формирования модели водной поверхности
В разделе 1 приводилось описание того, как строится поверхность. В этом
разделе будет рассмотрен алгоритм построения, а его также программная
реализация.
Программный комплекс состоит из следующих модулей:
 ssau.knyazev.ocean.Ocean – основной исполняемый класс;
 ssau.knyazev.ocean.OceanConst – интерфейс с константами;
 ocean.cu – файл с исходный кодом программы, исполняемой на GPU
устройстве. В нем реализованы методы для генерации спектра в момент
времени 𝑡 и расчета координат вершин, используемые при построении модели;
 один из подключаемых модулей для выполнения преобразования
Фурье (в зависимости от выбранного алгоритма).
Ниже для наглядности приведена схема работы комплекса в целом
(рисунок 8), а также краткое описание каждого из блоков.
48
Инициализация
Генерация начального спектра водной
поверхности – ℎ̃0
Замер
времени 1
Новый кадр
Генерация спектра водной поверхности в
момент времени t – ℎ̃(𝑡)
Обратное преобразование Фурье над
спектром водной поверхности ℎ̃(𝑡)
Построение 3D-модели водной
поверхности
Замер
времени 2
Завершение работы
Рисунок 8 – Схема работы программного комплекса
Приведем описание блоков схемы:
1.
«Инициализация» – создание контекста Cuda и установление связи
между Cuda и OpenGL. Создание VBO-объектов для последующего построения
3D-модели.
Используемые методы:
 private void initShaders(GL2 gl);
 private void initJCuda();
 private void initVBO(GL2 gl);
2.
«Замер времени 1» – замер текущего времени.
49
3.
̃𝟎» –
«Генерация начального спектра водной поверхности – 𝒉
Расчет спектра Филипса и наложение на него шума.
Используемые методы:
 private float[] generate_h0(float[] h0);
 private float phillips(float Kx, float Ky, float Vdir, float V, float A,
float dir_depend);
4.
«Новый кадр» – начало расчета очередного кадра анимации
поверхности воды.
5.
«Генерация спектра водной поверхности в момент времени t –
̃ (𝒕)» – расчет спектра водной поверхности в момент времени. Выполняется на
𝒉
GPU устройстве в модуле ocean.cu.
Используемый метод:
 extern "C" __global__ void generateSpectrumKernel(float2* h0,
float2 *ht,
unsigned int in_width,
unsigned int out_width,
unsigned int out_height,
float t, float patchSize);
6.
«Обратное
преобразование
Фурье
над
спектром
водной
̃ (𝒕)» – обратное двумерное ДПФ над спектром водной
поверхности 𝒉
поверхности, полученным в пункте 4. Выполняется на GPU устройстве в одном
из модулей, в зависимости от выбранного алгоритма: fft-module.cu, fft-b2module.cu и fft-c-module.cu.
7.
«Построение 3D-модели водной поверхности» – построение 3D-
модели водной поверхности по расчитанной карте высот. Далее, если
программа еще не завершает свое выполнение, то переходим к пункту 4.
Используемые методы:
 public void display(GLAutoDrawable drawable);
50
 extern "C" __global__ void updateHeightmapKernel(float* heightMap,
float2* ht,
unsigned int width);
 extern "C" __global__ void calculateSlopeKernel(float* h,
float2 *slopeOut,
unsigned int width,
unsigned int height);
8.
«Замер времени 2» – замер текущего времени.
9.
«Завершение работы» – подсчет времени работы программы и вывод
результатов. Программа завершает свою работу после выполнения некоторого
числа кадров. В данной работе количество кадров равно 1000. Время работы
программы вычисляется как разность значений полученных с помощью «Замер
времени 2» и «Замер времени 1». А средний показатель числа кадров в
секунду равен частному общему количеству кадров на затраченное время.
Далее
будут
представлены
программные
реализации
алгоритмов
дискретного преобразования Фурье, описание которых приведено в разделе 2.
3.4 Общие сведения для алгоритмов двумерного случая
Дискретное преобразование Фурье выполняется в разных алгебрах.
Поэтому необходимо было ввести классы-обертки над числами каждой из
алгебр.
Обычные
комплексные
ssau.knyazev.core.Complex,
числа
представлены
гиперкомплексные
числа
классом-оберткой
оборачиваются
ssau.knyazev.core.CComplex и наконец, прямая сумма комплексных чисел
представлена типом ssau.knyazev.core.B2Complex.
В процессе вычисления ДПФ основными операциями являются сложение
чисел в соответсвующей алгебре и умножение на степени корней 𝜔1 , 𝜔2 . Для
того, чтобы не вычислять синус и косинус для каждого набора коэффициентов
𝑚1 𝑛1 и 𝑚2 𝑛2 , , вычислим значения этих функций заранее и поместим в вектора,
51
а дальше будем просто высчитывать индекс используемого поворотного
коэффициента.
Также
был
написан
модуль
для
генерации
случайных
чисел
ssau.knyazev.modules.Generator, в котором реализованы методы для создания
вектора или матрицы состоящей из случайных чисел, из выбранного диапазона.
Текст исходного кода программных модулей приведен в Приложении В.
3.5 Программная реализация последовательного алгоритма
двумерного ДПФ
В данном подразделе будет рассмотрена реализация последовательного
алгоритма двумерного ДПФ, описанного в подразделе 2.4.2.
Реализовано в классах ssau.knyazev.fft.modules.oob.FFTModule для случая
работы программы над матрицей отсчетов в виде классов-оберток над
комплексными числами и ssau.knyazev.fft.modules.real.RealFFTModule в случае
работы над матрицей вещественных чисел. Причем обрабатываемая матрица в
таком случае будет состоять из 𝑁 строк и 2𝑁 столбцов, где 2 ∙ 𝑖 – индекс
действительной части -го отсчета, а 2 ∙ 𝑖 + 1 –индекс мнимой части
комплексного числа.
Выполняется методами:
 public static Complex[][] FFTModule.fft2D(float[][] src);
 public static Complex[][] FFTModule.fft2D(float[][] src, 0);
 public static Complex[][] FFTModule.fft2D(Complex[][] src);
 public static Complex[][] FFTModule.inverseFFT2D(Complex[][] src);
 public static Complex[][] FFTModule.fft2D(Complex[][] src, 0);
 public static Complex[][] FFTModule.fft2D(Complex[][] src, 0,
boolean inverse);
Если преобразование Фурье вычисляется с применением специальных
алгебр,
то
исполняемыми
52
классами
будут
и
ssau.knyazev.fft.modules.oob.B2FFTModule
ssau.knyazev.fft.modules.oob.CFFTModule при использовании прямой суммы
комплексных чисел и гиперкомплексных чисел соответственно. Если расчеты
ведутся без применения классов-оберток, а только лишь с примитивными
типами
float,
то
исполняемыми
классами
будут
и
ssau.knyazev.fft.modules.real.RealB2FFTModule
ssau.knyazev.fft.modules.real.RealCFFTModule.
Сам алгоритм для наглядности представим в виде схемы изображенной на
рисунке 12.
Инициализация
Замер
времени 1
Перестановка
Преобразование в
четырехмерную алгебру
ДПФ
Замер
времени 2
Завершение работы
Рисунок 12 – Схема последовательного алгоритма по схеме Кули-Тьюки
Приведем описание блоков схемы:
1.
«Инициализация» – создание и заполнение матрицы случайными
числами (генерируется методами класса ssau.knyazev.modules.Generator). Если в
качестве параметра передаются вещественные числа типа float, то они
53
оборачивается
классом-оберткой
заполняется
вектор
И
ssau.knyazev.core.Complex.
поворотных
коэффициентов,
наконец,
используемый
непосредственно при преобразовании.
2.
«Замер времени 1» – замер текущего времени.
3.
«Перестановка» – производится перестановка строк и столбцов
матрицы, полученной в пунке 1, в порядке двоичной инверсии.
4.
«Перевод
в
четырехмерную
алгебру»
-
в
случае,
если
преобразование Фурье выполняется с помощью четырехмерных алгебр
(гиперкомплексные числа или прямая сумма комплексных чисел), то исходная
матрица погружается пространство гиперкомплексных чисел или прямую
сумму комплексных чисел.
5.
«ДПФ» – дискретное двумерное преобразование Фурье матрицы.
6.
«Замер времени 2» – замер текущего времени.
7.
«Завершение работы» – подсчет времени работы программы и вывод
результатов. Время работы программы вычисляется как разность значений,
полученных при замере времени.
3.6 Программная реализация последовательного
построчно-столбцового алгоритма двумерного ДПФ
В данном подразделе будет рассмотрена реализация последовательного
алгоритма двумерного ДПФ, описанного в подразделе 2.4.1.
Реализовано в классе ssau.knyazev.fft.modules.oob.FFTModule.
Выполняется методами:
 public static Complex[][] FFTModule.fft2D(float[][] src, 1);
 public static Complex[][] FFTModule.fft2D(Complex[][] src, 1);
 public static Complex[][] FFTModule.fft2D(Complex[][] src, 1,
boolean inverse);
54
 protected static Complex[][] fft2DRowCol(Complex[][] src).
В построчно-столбцовом алгоритме используются одномерные
преобразования Фурье. Методы, которые выполняют данное преобразование,
перечислены ниже:
 public static Complex[] fft1D(Complex[] src);
 public static Complex[] inverseFFT1D(Complex[] src);
 public static Complex[] fft1D(Complex[] src, boolean inverse).
Сам алгоритм для наглядности представлен в виде схемы, изображенной
на рисунке 13.
Инициализация
Замер
времени 1
Перестановка
ДПФ каждой строки
ДПФ каждого столбца
Замер
времени 2
Завершение работы
Рисунок 13 – Схема последовательный построчно-столбцового алгоритма ДПФ
Приведем описание блоков схемы:
1.
«Инициализация» – создание и заполнение матрицы случайными
числами (генерируется методами класса ssau.knyazev.modules.Generator). Если в
качестве параметра передаются вещественные числа типа float, то они
55
оборачивается
классом-оберткой
заполняется
вектор
И
ssau.knyazev.core.Complex.
поворотных
коэффициентов,
наконец,
используемый
непосредственно при преобразовании.
2.
«Замер времени 1» – замер текущего времени.
3.
«Перестановка» – производится перестановка строк и столбцов
матрицы, полученной в пунке 1, в порядке двоичной инверсии.
4.
«ДПФ каждой строки» – дискретное одномерное преобразование
Фурье над всеми строками матрицы отсчетов.
5.
«ДПФ каждого столбца» – дискретное одномерное преобразование
Фурье над всеми столбцами матрицы отсчетов.
6.
«Замер времени 2» – замер текущего времени.
7.
«Завершение работы» – подсчет времени работы программы и вывод
результатов. Время работы программы вычисляется как разность значений,
полученных при замере времени.
3.7 Программная реализация алгоритма двумерного ДПФ с
распараллеливанием за счет структуры алгебры
В данном подразделе будет рассмотрена реализация алгоритма двумерного
ДПФ с распараллеливанием за счет структуры алгебры, описанным в
подразделе 2.5.3.
Реализовано в классе ssau.knyazev.fft.modules.CPUFFTModule.
Выполняется методом:
 public static Complex[][] fft2DB2Complex(float[][] src).
Сам алгоритм для наглядности представлен в виде схемы, изображенной
на рисунке 14.
56
Инициализация
Замер
времени 1
Перестановка
Преобразование в алгебру
Инициализация
процесса 1
Инициализация
процесса 2
ДПФ 1
ДПФ 2
Синхронизация процессов
Замер
времени 2
Завершение работы
Рисунок 14 – Алгоритм двумерного ДПФ с распараллеливанием
за счет структуры алгебры
Приведем описание блоков схемы:
1.
числами
«Инициализация» – создание и заполнение матрицы случайными
(генерируется
Заполняется
вектор
методами
класса
поворотных
ssau.knyazev.modules.Generator).
коэффициентов,
используемый
непосредственно при преобразовании.
2.
«Замер времени 1» – замер текущего времени.
3.
«Перестановка» – производится перестановка строк и столбцов
матрицы, полученной в пунке 1, в порядке двоичной инверсии.
57
«Преобразование в алгебру» – если исходная матрица представлена
4.
в виде вещественных чисел, то происходит ее преобразование в матрицу,
состоящую из чисел типа B2Comlex.
«Инициализация
5.
процесса
1»
–
создание
процесса
с
2»
–
создание
процесса
с
идентификационным номером 1.
«Инициализация
6.
процесса
идентификационным номером 2.
«ДПФ
7.
1»
–
процесс
1
выполняет
дискретное
двумерное
преобразование Фурье матрицы, оперируя с коэффициентами 𝑢0 и 𝑢2.
«ДПФ
8.
2»
–
процесс
2
выполняет
дискретное
двумерное
преобразование Фурье матрицы, оперируя с коэффициентами 𝑢1 и 𝑢3.
«Синхронизация процессов» – синхронизируется завершение работы
9.
обоих процессов.
10.
«Замер времени 2» – замер текущего времени.
11. «Завершение работы» – подсчет времени работы программы и вывод
результатов. Время работы программы вычисляется как разность значений,
полученных при замере времени.
3.8 Программная реализация алгоритма двумерного ДПФ с
распараллеливанием по структуре декомпозиции
В данном подразделе будет рассмотрена реализация алгоритма двумерного
ДПФ с распараллеливанием по структуре декомпозиции, описанным в
подразделе 2.5.2.
Реализовано в классе ssau.knyazev.fft.modules.CPUFFTModule.
Выполняется методом:
 public static Complex[][] fft2DByDecomposition(float[][] src,
int Algebratype).
58
Сам алгоритм для наглядности представлен в виде схемы, изображенной
на рисунке 15.
Инициализация
Замер
времени 1
Перестановка
Преобразование в алгебру
Инициализация
процесса (0, 0)
Инициализация
процесса (0, 1)
Инициализация
процесса (1, 0)
Инициализация
процесса (1, 1)
ДПФ над 𝑀00
ДПФ над 𝑀01
ДПФ над 𝑀10
ДПФ над 𝑀11
Синхронизация процессов
Замер
времени 2
Завершение работы
Рисунок 15 – Алгоритм двумерного ДПФ с распараллеливанием по структуре декомпозиции
Приведем описание блоков схемы:
1.
числами
«Инициализация» – создание и заполнение матрицы случайными
(генерируется
Заполняется
вектор
методами
класса
поворотных
ssau.knyazev.modules.Generator).
коэффициентов,
непосредственно при преобразовании.
2.
«Замер времени 1» – замер текущего времени.
59
используемый
3.
«Перестановка» – производится перестановка строк и столбцов
матрицы, полученной в пунке 1, в порядке двоичной инверсии.
4.
«Преобразование в алгебру» – если исходная матрица представлена
в виде вещественных чисел, то происходит ее преобразование в матрицу,
состоящую из чисел типа Complex, CComplex, B2Complex (в зависимости от
выбранной алгебры).
5.
«Инициализация
процесса
(0,
0)»
–
создание
процесса
с
(1,
0)»
–
создание
процесса
с
(0,
1)»
–
создание
процесса
с
(1,
1)»
–
создание
процесса
с
идентификационным номером (0, 0).
6.
«Инициализация
процесса
идентификационным номером (1, 0).
7.
«Инициализация
процесса
идентификационным номером (0, 1).
8.
«Инициализация
процесса
идентификационным номером (1, 1).
9.
«ДПФ над 𝑴𝟎𝟎 » – процесс (0, 0) выполняет дискретное двумерное
𝑁
преобразование Фурье над отсчетами матрицы 𝑋𝑖,𝑗 , где 𝑖 ∈ [0; − 1] , 𝑗 ∈
2
𝑁
[0; − 1].
2
10. «ДПФ над 𝑴𝟏𝟎 » – процесс (1, 0) выполняет дискретное двумерное
𝑁
𝑁
2
2
преобразование Фурье над отсчетами матрицы 𝑋𝑖,𝑗 , где 𝑖 ∈ [ ; 𝑁] , 𝑗 ∈ [0; −
𝑁].
11. «ДПФ над 𝑴𝟎𝟏 » – процесс (0, 1) выполняет дискретное двумерное
𝑁
преобразование Фурье над отсчетами матрицы 𝑋𝑖,𝑗 , где 𝑖 ∈ [0; − 1] , 𝑗 ∈
2
𝑁
[ − 1; 𝑁].
2
12. «ДПФ над 𝑴𝟏𝟏 » – процесс (1, 1) выполняет дискретное двумерное
𝑁
преобразование Фурье над отсчетами матрицы 𝑋𝑖,𝑗 , где 𝑖 ∈ [ − 1; 𝑁] , 𝑗 ∈
2
𝑁
[ − 1; 𝑁].
2
60
13. «Синхронизация процессов» – синхронизируется завершение работы
всех процессов.
14.
«Замер времени 2» – замер текущего времени.
15. «Завершение работы» – подсчет времени работы программы и вывод
результатов. Время работы программы вычисляется как разность значений,
полученных при замере времени.
3.9 Программная
двумерного
ДПФ
реализация
с
гибридного
распараллеливанием
по
алгоритма
структуре
декомпозиции и распараллеливанием по структуре алгебры
В данном подразделе будет рассмотрена реализация алгоритма двумерного
ДПФ с распараллеливанием по структуре декомпозиции и распараллеливанием
по структуре алгебры.
Реализовано в классе ssau.knyazev.fft.modules.CPUFFTModule.
Выполняется методом:
 public static Complex[][] fft2DB2ComplexByDnA(float[][] src).
Сам алгоритм для наглядности представлен в виде схемы, изображенной
на рисунке 16.
61
Инициализация
Замер
времени 1
Перестановка
Преобразование в алгебру
Инициализация
процесса 1 (0, 0)
Инициализация
процесса 2 (0, 0)
…
Инициализация
процесса 1 (1, 1)
Инициализация
процесса 2 (1, 1)
ДПФ 1 над 𝑀00
ДПФ 2 над 𝑀00
…
ДПФ 1 над 𝑀11
ДПФ 2 над 𝑀11
Синхронизация процессов
Замер
времени 2
Завершение работы
Рисунок 16 – Схема гибридного алгоритма двумерного ДПФ
Приведем описание блоков схемы:
1.
числами
«Инициализация» – создание и заполнение матрицы случайными
(генерируется
Заполняется
вектор
методами
класса
поворотных
ssau.knyazev.modules.Generator).
коэффициентов,
используемые
непосредственно при преобразовании.
2.
«Замер времени 1» – замер текущего времени.
3.
«Перестановка» – производится перестановка строк и столбцов
матрицы, полученной в пунке 1, в порядке двоичной инверсии.
62
4.
«Преобразование в алгебру» – если исходная матрица представлена
в виде вещественных чисел, то происходит ее преобразование в матрицу,
состоящую из чисел типа B2Complex.
5.
«Инициализация процесса 1 (0, 0)» – создание процесса с
идентификационным номером 1 (0, 0).
6.
«Инициализация процесса 2 (0, 0)» – создание процесса с
идентификационным номером 2 (0, 0).
7.
«Инициализация процесса 1 (1, 1)» – создание процесса с
идентификационным номером 1 (1, 1).
8.
«Инициализация процесса 2 (1, 1)» – создание процесса с
идентификационным номером 2 (1, 1).
9.
«ДПФ 1 над 𝑴𝟎𝟎 » – процесс 1 (0, 0) выполняет дискретное
двумерное преобразование Фурье, оперируя с коэффициентами 𝑢0 и 𝑢2, над
𝑁
𝑁
2
2
отсчетами матрицы 𝑋𝑖,𝑗 , где 𝑖 ∈ [0; − 1] , 𝑗 ∈ [0; − 1].
10. «ДПФ 2 над 𝑴𝟎𝟎 » – процесс 2 (0, 0) выполняет дискретное двумерное
преобразование Фурье, оперируя с коэффициентами 𝑢1 и 𝑢3, над отсчетами
𝑁
𝑁
2
2
матрицы 𝑋𝑖,𝑗 , где 𝑖 ∈ [0; − 1] , 𝑗 ∈ [0; − 1].
11. «ДПФ 1 над 𝑴𝟏𝟏 » – процесс 1 (1, 1) выполняет дискретное двумерное
преобразование Фурье, оперируя с коэффициентами 𝑢0 и 𝑢2, над отсчетами
𝑁
𝑁
2
2
матрицы 𝑋𝑖,𝑗 , где 𝑖 ∈ [ − 1; 𝑁] , 𝑗 ∈ [ − 1; 𝑁].
12. «ДПФ 2 над 𝑴𝟏𝟏 » – процесс 2 (1, 1) выполняет дискретное двумерное
преобразование Фурье, оперируя с коэффициентами 𝑢1 и 𝑢3, над отсчетами
𝑁
𝑁
2
2
матрицы 𝑋𝑖,𝑗 , где 𝑖 ∈ [ − 1; 𝑁] , 𝑗 ∈ [ − 1; 𝑁].
13. «Синхронизация процессов» – синхронизируется завершение работы
всех процессов.
14.
«Замер времени 2» – замер текущего времени.
63
15. «Завершение работы» – подсчет времени работы программы и вывод
результатов. Время работы программы вычисляется как разность значений,
полученных при замере времени.
В такой реализации участвуют 8 процессов. Пропущенные процессы
процесс 1 (1, 0), процесс 2 (1, 0), процесс 1 (0, 1), процесс 2 (0, 1) выполняются
аналогично пунктам 5-10.
3.10 Программная
реализация
ДПФ
с
постоянной
синхронизацией процессов и без
В предыдущих реализациях, процессы работают независимо друг от друга,
синхронизируясь лишь на последней итерации. В данном разделе будет
приведена схема с постоянной синхронизацией процессов на каждом шаге.
Такая реализация окажется крайне удачной и будет реализована с помощью
связки технологий
Cuda + JCuda.
Реализовано в классе ssau.knyazev.fft.modules.CPUFFTModule. А также с
технологией Cuda в классе ssau.knyazev.fft.modules.GPUFFTModule, исходный
код исполняемый на видеокарте: fft-module.cu, fft-b2-module.cu и fft-c-module.cu.
Выполняется методами класса ssau.knyazev.fft.modules.CPUFFTModule:
 public static float[][] fft2DComplexWithSync(float[][] src);
 public static float [][] fft2DCComplexWithSync(float[][] src);
 public static float [][] fft2DB2ComplexWithSync(float[][] src);
и методами класса ssau.knyazev.fft.modules.GPUFFTModule:
 public static float[][] fft2DComplex(float[][] src);
 public static void fft2DComplex(CUdeviceptr src);
 public static float [][] fft2DCComplex(float[][] src);
 public static void fft2DCComplex(CUdeviceptr src);
 public static float [][] fft2DB2Complex(float[][] src);
 public static void fft2DB2Complex(CUdeviceptr src).
64
На рисунке 17 показан пример выполнения 4 процессов.
Инициализация
Замер
времени 1
Перестановка
Преобразование в алгебру
Инициализация
процесса (0, 0)
Инициализация
процесса (0, 1)
Инициализация
процесса (1, 0)
Инициализация
процесса (1, 1)
Следующая итерация
Бабочка над
0, 4, 8, … , 𝑁 − 4
четверкой отсчетов
Бабочка над
1, 5, 9, … , 𝑁 − 3
четверкой отсчетов
Бабочка над
2, 6, 10, … , 𝑁 − 2
четверкой отсчетов
Бабочка над
3, 7, 11, … , 𝑁 − 1
четверкой отсчетов
Синхронизация процессов
Замер
времени 2
Завершение работы
Рисунок 17 – Алгоритм двумерного ДПФ по схеме Кули-Тьюки с
постоянной синхронизацией процессов
Приведем описание блоков схемы:
1.
числами
«Инициализация» – создание и заполнение матрицы случайными
(генерируется
методами
класса
65
ssau.knyazev.modules.Generator).
Заполняется
вектор
поворотных
коэффициентов,
используемые
непосредственно при преобразовании.
2.
«Замер времени 1» – замер текущего времени.
3.
«Перестановка» – производится перестановка строк и столбцов
матрицы, полученной в пунке 1, в порядке двоичной инверсии.
4.
«Преобразование в алгебру» – если исходная матрица представлена
в виде вещественных чисел, то происходит ее преобразование в матрицу,
состоящую из чисел типа Complex, CComplex, B2Complex (в зависимости от
выбранной алгебры).
5.
«Инициализация
процесса
(0,
0)»
–
создание
процесса
с
(1,
0)»
–
создание
процесса
с
(0,
1)»
–
создание
процесса
с
(1,
1)»
–
создание
процесса
с
идентификационным номером (0, 0).
6.
«Инициализация
процесса
идентификационным номером (1, 0).
7.
«Инициализация
процесса
идентификационным номером (0, 1).
8.
«Инициализация
процесса
идентификационным номером (1, 1).
9.
«Новая итерация» – Очередная итерация в преобразовании Фурье.
10. «Бабочка над 𝟎, 𝟒, 𝟖, … , 𝑵 − 𝟒 четверкой отсчетов» – процесс (0, 0)
производит умножение и сложение с 0, 4, 8, … , 𝑁 − 4 четверкой отсчетов.
11. «Бабочка над 𝟏, 𝟓, 𝟗, … , 𝑵 − 𝟑 четверкой отсчетов» – процесс (1, 0)
производит умножение и сложение с 1, 5, 9, … , 𝑁 − 3 четверкой отсчетов.
12. «Бабочка над 𝟐, 𝟔, 𝟏𝟎, … , 𝑵 − 𝟐 четверкой отсчетов» – процесс (0, 1)
производит умножение и сложение с 2, 6, 10, … , 𝑁 − 2 четверкой отсчетов.
13. «Бабочка над 𝟑, 𝟕, 𝟏𝟏, … , 𝑵 − 𝟏 четверкой отсчетов» – процесс (1, 1)
производит умножение и сложение с 3, 7, 11, … , 𝑁 − 1 четверкой отсчетов.
14.
«Синхронизация
процессов»
–
синхронизируется
завершение
работы всех процессов. Если все итерации пройдены, то программа завершает
свое выполнение, иначе возвращаемся к пункту 9.
15. «Замер времени 2» – замер текущего времени.
66
16. «Завершение работы» – подсчет времени работы программы и вывод
результатов. Время работы программы вычисляется как разность значений,
полученных при замере времени.
67
4 Экспериментальные исследования алгоритмов и анализ
результатов
При помощи программного комплекса, разработанного в процессе
выполнения работы, были проведены серии экспериментов.
Исследования проводились как над реализованными алгоритмами БПФ,
так и над программным комплексом в целом. В свою очередь, исследование
алгоритмов БПФ разделены на две части: в первой части реализованные
алгоритмы тестировались на многоядерном центральном процессоре, а во
второй части на графическом процессоре.
В
качестве
входных
данных
использовалась квадратная
матрица,
состоящая из вещественных чисел, генерируемых случайным образом.
В результате были получены значения времени, затрачиваемое на
вычисление преобразования Фурье при помощи определенного алгоритма, а
также значение скорости прорисовки анимации 3D-модели.
Программный
комплекс
тестировался
на
системе
конфигурацией:

процессор: AMD Phenom II X4 965 @ 3.40 Ghz;

оперативная память: 4 Gb;

видеокарта: NVidia GeForce 450 GTS 1 Gb.
Используемое программное обеспечение:

операционная система: Windows 7 Максимальная;

NVIDIA DevDriver 301.32;

CudaToolkit 4.1.28;

NVIDIA GPU Computing SDK 4.1;

JDK 1.7.0;

JCuda 0.4.1.
68
со
следующей
4.1 Исследование скорости выполнения алгоритмов на CPU
В данном разделе исследуется скорость выполнения алгоритмов ДПФ на
центральном
процессоре.
Каждый
из
алгоритмов
выполняется
как
последовательно (1 процесс), так и параллельно (2, 4 процесса).
Построчно-столбцовый
алгоритм
(комплексные
числа).
Время
выполнения алгоритма приводится в таблице 3. Для визуального представления
также приведен график на рисунке 18.
Таблица 3 - Время работы последовательного и параллельного построчно-столбцового
алгоритма двумерного ДПФ
Размер
1 процесс
2 процесса
4 процесса
Время, мс
Время, мс
Время, мс
8
0,515228
0,59813
0,798626
16
0,74007
0,749365
0,925484
32
1,353672
1,117226
1,18085
64
4,226612
2,488438
2,973053
128
15,610465
8,072981
8,175732
256
74,563327
37,2345
35,121239
512
353,893261
184,948027
148,41198
1024
1517,954506
907,041146
630,813882
2048
9531,025981
5385,938665
3661,409248
4096
41395,98399
24099,89818
16595,41074
69
45000
40000
35000
30000
25000
20000
15000
10000
5000
0
0
500
1000
1500
2000
1 процесс
2500
2 процесса
3000
3500
4000
4500
4 процесса
Рисунок 18 –Время выполнения построчно-столбцового алгоритма
ДПФ (комплексные числа)
Из таблицы 3 следует, что среднее значение ускорения параллельного
построчно-столбцового
алгоритма
двумерного
ДПФ
относительного
последовательного алгоритма равно 1,74 и 2,061 для 2 и 4 процессов
соответственно.
Алгоритм
по
схеме
Кули-Тьюки
(комплексные
числа).
Время
выполнения алгоритма приводится в таблице 4. Для визуального представления
также приведен график на рисунке 19.
70
Таблица 4 – Время работы последовательного и параллельного алгоритма двумерного ДПФ
по схеме Кули-Тьюки
Размер
1 процесс
2 процесса
4 процесса
Время, мс
Время, мс
Время, мс
8
0,574773
0,584278
0,698873
16
0,784655
0,86327
0,848908
32
1,313255
1,114228
1,261474
64
3,654897
2,408893
2,651096
128
13,337874
7,773391
7,782655
256
62,847353
35,762575
33,891906
512
283,133216
164,77343
137,447949
1024
1274,520691
772,609426
622,614283
2048
6051,432017
3741,407473
2961,397645
4096
26932,53608
16671,89694
13248,04979
30000
25000
20000
15000
10000
5000
0
0
500
1000
1500
1 процесс
2000
2500
2 процесса
3000
3500
4000
4500
4 процесса
Рисунок 19 – Время выполнения ДПФ по алгоритму Кули-Тьюки (комплексные числа)
Из таблицы 4 следует, что среднее значение ускорения параллельного
выполнения двумерного ДПФ по алгоритму Кули-Тьюки относительного
71
последовательного алгоритма равно 1,5962 и 1,7714 для 2 и 4 процессов
соответственно.
Алгоритм по схеме Кули-Тьюки с постоянной синхронизацией
процессов (комплексные числа). Время выполнения алгоритма приводится в
таблице 5. Для визуального представления также приведен график на рисунке
19.
Таблица 5 – Время работы последовательного и параллельного алгоритма двумерного ДПФ
по схеме Кули-Тьюки с постоянной синхронизацией процессов
Размер
1 процесс
2 - процесса
4 - процесса
Время, мс
Время, мс
Время, мс
8
0,533667
0,585717
0,737821
16
0,787503
0,876792
0,86309
32
1,343118
1,12868
1,135425
64
3,61352
2,372824
3,179996
128
13,669096
7,691747
7,785174
256
64,01483
35,239492
31,747222
512
283,918321
165,735794
130,596022
1024
1271,905845
773,191695
589,442288
2048
6021,031391
3737,298544
2732,80889
4096
26896,6452
16730,82045
12835,36278
72
30000
25000
20000
15000
10000
5000
0
0
500
1000
1500
1 процесс
2000
2500
2 процесса
3000
3500
4000
4500
4 процесса
Рисунок 20 – Время выполнения ДПФ по схеме Кули-Тьюки с постоянной синхронизацией
процессов (комплексные числа)
Из таблицы № следует, что среднее значение ускорения параллельного
выполнения двумерного ДПФ по алгоритму Кули-Тьюки с постоянной
синхронизацией относительного последовательного алгоритма равно 1,6008 и
1,8302 для 2 и 4 процессов соответственно.
Алгоритм по схеме Кули-Тьюки (алгебра гиперкомплексных чисел).
Время выполнения алгоритма приводится в таблице 6. Для визуального
представления также приведен график на рисунке 21.
73
Таблица 6 – Время работы последовательного и параллельного алгоритма двумерного ДПФ
по схеме Кули-Тьюки
Размер
1 процесс
2 процесса
4 процесса
Время, мс
Время, мс
Время, мс
8
0,672494
0,68002
0,734259
16
0,942043
0,733318
0,98477
32
1,464712
1,418657
1,371704
64
3,95563
3,24077
3,012267
128
14,566704
11,643757
9,663452
256
68,958366
44,670131
38,750327
512
330,078631
219,578165
184,338636
1024
1373,240515
955,676984
748,661262
2048
5952,702585
4513,459392
3608,435308
4096
33317,90015
17002,58979
14112,00352
35000
30000
25000
20000
15000
10000
5000
0
0
500
1000
1500
1 процесс
2000
2500
2 процесса
3000
3500
4 процесса
Рисунок 21 – Время выполнения ДПФ по алгоритму Кули-Тьюки
(алгебра гиперкомплексных чисел)
74
4000
4500
Из таблицы 6 следует, что среднее значение ускорения параллельного
выполнения двумерного ДПФ по алгоритму Кули-Тьюки относительного
последовательного алгоритма равна 1,4083 и 1,6689 для 2 и 4 процессов
соответственно.
Алгоритм по схеме Кули-Тьюки с учетом симметрии спектра (алгебра
гиперкомплексных чисел). Время выполнения быстрого алгоритма приводится
в таблице 7. Для визуального представления также приведен график на рисунке
22, на котором помимо табличных данных из таблицы 7 показан график
времени выполнения ГДПФ без учета симметрии на 4 процессах.
Таблица 7 – Время работы последовательного и параллельного алгоритма двумерного ДПФ
по схеме Кули-Тьюки с учетом симметрии спектра
Размер
1 процесс
2 процесса
4 процесса
Время, мс
Время, мс
Время, мс
8
0,600861
0,671212
0,744267
16
0,829938
0,829874
0,81775
32
1,392253
1,168565
1,025924
64
3,016298
2,470778
2,150192
128
10,040015
7,642538
6,282182
256
43,224153
29,319883
25,864024
512
221,934934
144,591515
125,216848
1024
907,133965
634,799305
493,813375
2048
4183,207224
2653,857531
2162,185734
4096
20753,24972
14756,45609
10619,13472
75
25000
20000
15000
10000
5000
0
0
500
1000
1 процесс
1500
2000
2 процесса
2500
3000
4 процесса
3500
4000
4500
ГДПФ (4 процесса)
Рисунок 22 – Время выполнения алгоритма ДПФ по алгоритму Кули-Тьюки с учетом
симметрии спектра (алгебра гиперкомплексных чисел)
Алгоритм по схеме Кули-Тьюки (прямая сумма комплексных алгебр).
Время выполнения быстрого ДПФ по алгоритму Кули-Тьюки над отсчетами в
прямой сумме комплексных алгебр приводится в таблице 8. Также в таблицу
внесены результаты с распараллеливанием по структуре алгебры на 2 процесса.
Для визуального представления также приведен график на рисунке 23.
Таблица 8 – Время работы последовательного и параллельного алгоритма двумерного ДПФ
по схеме Кули-Тьюки
1 процесс
Размер
8
16
32
64
128
256
512
1024
2048
4096
Время, мс
0,475891
0,954751
1,747174
5,732735
20,708356
91,163505
432,066458
1779,65201
8281,161808
39485,89506
ПСА - 2 процесса
Время, мс
0,785402
1,097277
1,783587
5,256227
17,720785
63,876286
293,969508
1149,598828
5339,293824
25085,08769
76
2 процесса
4 процесса
Время, мс
0,77104
1,082892
1,790819
4,703418
17,102388
63,542767
304,57695
1247,186494
5807,936654
27494,10498
Время, мс
0,832955
1,167414
2,060275
4,6156
14,008792
59,677042
270,78196
1040,823552
4851,677423
22987,15834
45000
40000
35000
30000
25000
20000
15000
10000
5000
0
0
500
1000
1 процесс
1500
2000
2 процесса
2500
3000
4 процесса
3500
4000
4500
2 процесса (ПСА)
Рисунок 23 – Время выполнения ДПФ по алгоритму Кули-Тьюки
(прямая сумма комплексных алгебр)
Также для простоты сравнения эффективности алгоритмов между собой
приведены графики времени их выполнения для 1, 2 и 4 процессов в
Приложении Б на рисунках Б.1, Б.2 и Б.3.
4.2 Исследование скорости выполнения алгоритмов на GPU
В данном разделе исследуется скорость выполнения алгоритмов ДПФ на
видеокарте NVIDIA GeForce 450 GTS. Количество исполняемых потоков
зависит от размеров входной матрицы, например, для матрицы размером 25 ×
25 число потоков будет равным 210 . Однако, потоки – это логические процессы,
число же физических процессов будет намного меньше, поскольку их
количество не может быть больше числа исполняемых ядер на устройстве. На
данной видеокарте расположено 192 ядра, поэтому и число одновременно
исполняемых процессов будет равным 192.
77
Построчно-столбцовый
алгоритм.
Время
выполнения
построчно-
столбцового алгоритма приводится в таблице 9. Для сравнения также
приведены результаты, полученные в ходе исследования алгоритма на CPU.
Таблица 9 – Время работы построчно-столбцового алгоритма двумерного ДПФ на GPU
1 процесс
2 процесса
4 процесса
CUDA
Размер
8
16
32
64
128
256
512
1024
2048
Время, мс
0,515228
0,74007
1,353672
4,226612
15,610465
74,563327
353,893261
1517,954506
9531,025981
Время, мс
0,59813
0,749365
1,117226
2,488438
8,072981
37,2345
184,948027
907,041146
5385,938665
Время, мс
0,798626
0,925484
1,18085
2,973053
8,175732
35,121239
148,41198
630,813882
3661,409248
Время, мс
0,427319
0,526832
0,701184
2,264892
8,492318
35,387474
140,480071
412,29942
3128,112019
4096
41395,98399
24099,89818
16595,41074
14142,51138
Алгоритм по схеме Кули-Тьюки с постоянной синхронизацией
процессов (алгебра комплексных чисел). Время выполнения быстрого ДПФ по
алгоритму Кули-Тьюки с постоянной синхронизацией приводится в таблице 10.
Для сравнения также приведены результаты, полученные в ходе исследования
алгоритма на CPU.
Таблица 10 – Время работы алгоритма двумерного ДПФ по схеме Кули-Тьюки на GPU
1 процесс
2 процесса
4 процесса
Размер
8
16
32
64
128
256
512
1024
2048
Время, мс
0,533667
0,787503
1,343118
3,61352
13,669096
64,01483
283,918321
1271,905845
6021,031391
Время, мс
0,585717
0,876792
1,12868
2,372824
7,691747
35,239492
165,735794
773,191695
3737,298544
Время, мс
0,737821
0,86309
1,135425
3,179996
7,785174
31,747222
130,596022
589,442288
2732,80889
Время, мс
0,632313
0,692099
0,827982
1,02722
1,302945
1,916428
3,563669
8,141996
26,37282
4096
26896,6452
16730,82045
12835,36278
114,044406
78
CUDA
Алгоритм по схеме Кули-Тьюки с постоянной синхронизацией
процессов (алгебра гиперкомплексных чисел). Время выполнения быстрого
ДПФ по алгоритму Кули-Тьюки с постоянной синхронизацией приводится в
таблице 11. Для сравнения также приведены результаты, полученные в ходе
исследования алгоритма на CPU.
Таблица 11 – Время работы алгоритма двумерного ДПФ по схеме Кули-Тьюки на GPU
Размер
8
16
32
64
128
256
512
1024
2048
4096
1 процесс
2 процесса
4 процесса
Время, мс
0,672494
0,942043
1,464712
3,95563
14,566704
68,958366
330,078631
1373,240515
5952,702585
33317,90015
Время, мс
0,68002
0,733318
1,418657
3,24077
11,643757
44,670131
219,578165
955,676984
4513,459392
17002,58979
Время, мс
0,734259
0,98477
1,371704
3,012267
9,663452
38,750327
184,338636
748,661262
3608,435308
14112,00352
CUDA
Время, мс
0,755933
0,989016
1,172871
1,335468
1,487362
2,108847
2,73426
4,179646
9,608171
28,948367
Алгоритм по схеме Кули-Тьюки с учетом симметрии спектра (алгебра
гиперкомплексных чисел). Время выполнения быстрого ДПФ по алгоритму
Кули-Тьюки с постоянной синхронизацией приводится в таблице 12.
Таблица 12 – Время работы алгоритма двумерного ДПФ по схеме Кули-Тьюки с учетом
симметрии спектра на GPU
Размер
8
16
32
64
128
256
512
1024
2048
4096
1 процесс
2 процесса
4 процесса
Время, мс
0,600861
0,829938
1,392253
3,016298
10,040015
43,224153
221,934934
907,133965
4183,207224
20753,24972
Время, мс
0,671212
0,829874
1,168565
2,470778
7,642538
29,319883
144,591515
634,799305
2653,857531
14756,45609
Время, мс
0,744267
0,81775
1,025924
2,150192
6,282182
25,864024
125,216848
493,813375
2162,185734
10619,13472
79
CUDA
Время, мс
0,740581
0,979901
0,882533
0,860976
1,09269
1,266064
1,794496
2,5438
5,222815
16,883298
Алгоритм по схеме Кули-Тьюки с постоянной синхронизацией
процессов (прямая сумма комплексных алгебр). Время выполнения быстрого
ДПФ по алгоритму Кули-Тьюки с постоянной синхронизацией приводится в
таблице 13. Для сравнения также приведены результаты, полученные в ходе
исследования алгоритма на CPU.
Таблица 13 – Время работы алгоритма двумерного ДПФ по схеме Кули-Тьюки на GPU
1 процесс
Размер
8
16
32
64
128
256
512
1024
2048
4096
ПСА - 2 процесса
Время, мс
0,475891
0,954751
1,747174
5,732735
20,708356
91,163505
432,066458
1779,65201
8281,161808
39485,89506
2 процесса
4 процесса
Время, мс
0,77104
1,082892
1,790819
4,703418
17,102388
63,542767
304,57695
1247,186494
5807,936654
27494,10498
Время, мс
0,832955
1,167414
2,060275
4,6156
14,008792
59,677042
270,78196
1040,823552
4851,677423
22987,15834
Время, мс
0,785402
1,097277
1,783587
5,256227
17,720785
63,876286
293,969508
1149,598828
5339,293824
25085,08769
CUDA
Время, мс
0,732186
0,957537
1,150591
1,337417
1,511742
2,156865
2,775052
4,236497
9,330453
27,60724
Алгоритм по схеме Кули-Тьюки с учетом симметрии спектра (прямая
сумма комплексных алгебр). Время выполнения быстрого ДПФ по алгоритму
Кули-Тьюки с постоянной синхронизацией приводится в таблице 14. Для
сравнения также приведены результаты, полученные в ходе исследования
алгоритма на CPU.
Таблица 14 – Время работы алгоритма двумерного ДПФ по схеме Кули-Тьюки с учетом
симметрии спектра на GPU
1 процесс
Размер
8
16
32
64
128
256
512
1024
2048
4096
Время, мс
0,475891
0,954751
1,747174
5,732735
20,708356
91,163505
432,066458
1779,65201
8281,161808
39485,89506
ПСА - 2 процесса
Время, мс
0,785402
1,097277
1,783587
5,256227
17,720785
63,876286
293,969508
1149,598828
5339,293824
25085,08769
2 процесса
4 процесса
Время, мс
0,77104
1,082892
1,790819
4,703418
17,102388
63,542767
304,57695
1247,186494
5807,936654
27494,10498
Время, мс
0,832955
1,167414
2,060275
4,6156
14,008792
59,677042
270,78196
1040,823552
4851,677423
22987,15834
80
CUDA
Время, мс
0,740926
0,974491
0,872494
0,848601
1,071742
1,232811
1,739784
2,441934
4,927841
15,636526
Для сравнения эффективности алгоритмов ДПФ, реализованных с
помощью технологии CUDA, на рисунке 24 приведена наглядная диаграмма.
На диаграмме продемонстрировано время выполнения алгоритмов ДПФ над
матрицей
отсчетов
размером:
1024 × 1024,
2048 × 2048
и 4096 × 4096.
30
25
20
15
10
5
0
1024x1024
2048x2048
4096x4096
Схема Кули-Тьюки (комплексные числа)
Схема Кули-Тьюки (гиперкомплексные числа)
Схема Кули-Тьюки + учет симметрии (гиперкомплексные числа)
Схема Кули-Тьюки (прямая сумма комплексных алгебр)
Схема Кули-Тьюки + учет симметрии (прямая сумма комплексных алгебр)
Рисунок 24 – Сравнение эффективности алгоритмов ДПФ для матриц отсчетов размером
1024х1024, 2048х2048 и 4096х4096
4.3 Исследование скорости прорисовки модели водной
поверхности
В
данном
разделе
исследуется
прикладное
применение
быстрых
алгоритмов ДПФ при построении 3D-модели водной поверхности. Для каждого
из реализованных алгоритмов ДПФ с использованием технологии CUDA
приводятся средние значения числа кадров в секунду для разных размеров
81
матриц спектра водной поверхности. В таблицу 15 сведены результаты для
следующих алгоритмов:
 ПСА – построчно столбцовый алгоритм;
 КТ – ДПФ по схеме Кули-Тьюки (алгебра комплексных чисел);
 ГКТ – ДПФ по схеме Кули-Тьюки (алгебра гиперкомплексных чисел);
 ГКТ+симметрия – ДПФ по схеме Кули-Тьюки с учетом симметрии
спектра (алгебра гиперкомплексных чисел);
 ПСКТ – ДПФ по схеме Кули-Тьюки (прямая сумма комплексных
алгебр);
 ПСКТ+симметрия – ДПФ по схеме Кули-Тьюки с учетом симметрии
спектра (прямая сумма комплексных алгебр).
Таблица 15 – Скорость построения 3D-модели для каждого из алгоритмов
ПСА
КТ
ГКТ
ПСКТ
FPS
ГКТ+
симметрия
FPS
FPS
ПСКТ+
симметрия
FPS
FPS
FPS
8
550,8203367
494,9348369
466,3987672
469,7623378
471,6222532
469,6862167
16
524,7498255
482,8732115
422,3233358
423,9553422
428,0134824
424,9299609
32
476,5785473
464,7002103
389,1085407
438,6659467
392,5113544
440,6062742
64
272,2605686
439,8473554
364,4941841
440,715793
364,2354316
443,1325751
128
100,6049881
376,6197474
340,7260668
393,6640558
337,9190069
396,9373899
256
27,08067018
325,5091777
274,1167205
356,4683681
270,5555235
360,7445045
512
7,003114103
216,2027532
198,1058309
243,4249106
196,5177449
246,7106683
1024
1,620286981
121,1784409
110,4381135
134,7890733
109,7490522
136,6655492
2048
0,274505881
45,3293684
40,97461229
49,95004995
41,44624517
50,69701809
Размер
Как видно из результатов, погружение входных данных в специальную
алгебраическую структуру на размерах сетки 2048 × 2048 дает выигрыш в 4-5
кадров, тем самым давая более комфортный просмотр анимации. Ниже на
рисунке 25 показан пример построенной 3D-модели для размера сетки спектра
водной поверхности равной 2048 × 2048. Примеры работы программы для
других размеров сетки приведены в приложении Б.
82
Рисунок 25 – Пример работы программы с сеткой размером 2048х2048
83
ЗАКЛЮЧЕНИЕ
В данной работе были описаны быстрые алгоритмы дискретного
преобразования Фурье, а также способы построения 3D-модели водной
поверхности океана.
Написаны программные модули, реализующие представленные алгоритмы
ДПФ.
Написан
программный
комплекс,
строящий
3D-модель
водной
поверхности.
Исследования показали, что погружение входных данных в специальную
алгебраическую структуру позволяют выиграть в скорости вычисления ДПФ
относительно вычисления ДПФ, выполняемого во множестве комплексных
чисел, что в свою очередь ускоряет процесс построения 3D-модели водной
поверхности на 10-15%.
В результате проделанной работы сделаны следующие выводы:
1.
Все алгоритмы, выполняемые по схеме Кули-Тьюки, оказываются
значительно эффективнее построчно-столбцового алгоритма. Это особенно
ярко выражено при вычислении на графическом процессоре, когда время
работы снижается в 10-100 раз.
2.
Исполнение алгоритмов по схеме Кули-Тьюки с постоянной
синхронизацией оказывается на несколько миллисекунд быстрее (и этот
показатель растет вместе с увеличением числа исполняемых процессов и/или
размеров матрицы), чем независимое выполнение. Это связано с тем, что в
асинхронном выполнении объединение матриц выполняет только один
процесс, а остальные в это время простаивают. В свою очередь алгоритм с
постоянной синхронизацией имеет два недостатка. Во-первых, синхронизация
потоков на CPU является дорогостоящей операцией (на GPU потоки считаются
«легкими» и затраты на их синхронизацию незначительны). Во-вторых, потоки
вынуждены на каждом шаге итерации ждать завершения еще работающих
потоков, таким образом, простаивая некоторое время.
84
3.
Метод погружения входного сигнала в специальную алгебраическую
структуру является мощным средством повышения производительности
вычисления дискретного преобразования Фурье, а вместе с учетом симметрии
спектра дает выигрыш при построении 3D-модели на 10-15 %.
85
СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ
1.
Computer image processing // V.Soifer (ed), VDM Verlag Dr.Müller,
Saarbrücken, Germany, 2010.
2.
V.M. Chernov. Fast algorithms of Discrete Orthogonal Transforms for Data
Represented in Cyclotomic Fields // Pattern Recognition and Image Analysis. 1993.
Vol.3, No.4. P. 455-458.
3.
Jerry Tessendorf. Simulating Ocean Water // SIGGRAPH course notes. 2004.
4.
David Henry. On Gerstner’s Water Wave // Journal of Nonlinear Mathematical.
Physics. 2008. Vol. 15. Supplement 2. P. 87-95.
5.
Jason L. Mitchell. Real-Time Synthesis and Rendering of Ocean Water // ATI
Research Technical Report. April 2005.
6.
Chicheva M.A. Parallel implementation of multidimensional hypercomplex
DFT with regard for real type of input signal // 9-th International Conference on
Pattern Recognition and Image Analysis: New Information Technologies. (PRIA-92008). Nizhnii Novgorod, the Russian Federation Sptember 14-20, 2008. Conference
proceedings v1, p. 70-73.
7.
Чичева М.А. Компьютерная алгебра. Методические указания к курсовому
проектированию: учебное пособие. Самара.: Изд-во Самар. гос. аэрокосм. ун-та,
2007. 64с.
8.
Быстрое преобразование Фурье [Электронный ресурс]// dsplib.ru. Теория и
практика
цифровой
обработки
сигналов.
URL:
http://www.dsplib.ru/content/fft/fft.html (дата обращения: 14.04.2012).
9.
Тропченко А.Ю., Тропченко А.А. Цифровая обработка сигналов. Методы
предварительной обработки. Учебное пособие по дисциплине «Теоретическая
информатика». СПб.: СПбГУ ИТМО, 2009. 100 с.
10. steps3D - Tutorials - Основы CUDA [Электронный ресурс]// steps3D Графика, ООП. URL: http://steps3d.narod.ru/tutorials/cuda-tutorial.html (дата
86
обращения: 14.04.2012).
11. CUDA Zone | NVIDIA Developer Zone [Электронный ресурс]// Мировой
лидер в области технологий визуальных вычислений | NVIDIA. URL:
http://developer.nvidia.com/category/zone/cuda-zone (дата обращения: 14.04.2012).
12. Yonghong Yan, Max Grossman, Vivek Sarkar. JCUDA: A ProgrammerFriendly Interface for Accelerating Java Programs with Cuda. Department of
Computer Science. Rice University, 2009. 13p.
87
ПРИЛОЖЕНИЕ А
Графики сравнения алгоритмов ДПФ
45000
40000
35000
30000
25000
20000
15000
10000
5000
0
0
500
1000
1500
2000
2500
3000
3500
4000
Построчно-столбцовый алгоритм
Кули-Тьюки (комплексные числа)
Кули-Тьюки с синхронизацией (комплексные числа)
Кули-Тьюки (гиперкмоплексные числа)
Кули-Тьюки с учетом симметрии спектра (гиперкомплексные числа)
Кули-Тьюки (прямая сумма)
Рисунок А.1 – Сравнение алгоритмов, выполняемых последовательно
88
4500
30000
25000
20000
15000
10000
5000
0
0
500
1000
1500
2000
2500
3000
3500
4000
Построчно-столбцовый алгоритм
Кули-Тьюки (комплексные числа)
Кули-Тьюки с синхронизацией (комплексные числа)
Кули-Тьюки (гиперкмоплексные числа)
Кули-Тьюки с учетом симметрии спектра (гиперкомплексные числа)
Кули-Тьюки (пряммая сумма)
Кули-Тьюки с распараллеливанием по структуре алгебры (пряммая сумма)
Рисунок А.2 – Сравнение алгоритмов, выполняемых параллельно на 2 процессах
89
4500
25000
20000
15000
10000
5000
0
0
500
1000
1500
2000
2500
3000
3500
4000
4500
Построчно-столбцовый алгоритм
Кули-Тьюки (комплексные числа)
Кули-Тьюки с синхронизацией (комплексные числа)
Кули-Тьюки (гиперкмоплексные числа)
Кули-Тьюки с учетом симметрии спектра (гиперкомплексные числа)
Кули-Тьюки (пряммая сумма)
Рисунок А.3 – Сравнение алгоритмов ДПФ, выполняемых параллельно на 4 процессах
90
ПРИЛОЖЕНИЕ Б
Примеры работы программы для разных размеров сетки
Рисунок Б.1 – Пример работы программы с сеткой размером 512х512
91
Рисунок Б.2 – Пример работы программы с сеткой размером 128х128
92
ПРИЛОЖЕНИЕ В
Исходный текст программы
ssau.knyazev.ocean.OceanConst.java
package ssau.knyazev.ocean;
public interface OceanConst {
String vertexShaderSource ="varying vec3 eyeSpacePos;\n" +
"varying vec3 worldSpaceNormal;\n" +
"varying vec3 eyeSpaceNormal;\n" +
"uniform float heightScale; // = 0.5;\n" +
"uniform float chopiness; // = 1.0;\n" +
"uniform vec2 size;
// = vec2(256.0, 256.0);\n" +
"\n" +
"void main()\n" +
"{" +
" float height = gl_MultiTexCoord0.x;\n" +
" vec2 slope = gl_MultiTexCoord1.xy;\n" +
"\n" +
" // calculate surface normal from slope for shading\n" +
"
vec3 normal
= normalize(cross( vec3(0.0, slope.y*heightScale, 2.0 / size.x), vec3(2.0 /
size.y, slope.x*heightScale, 0.0)));\n" +
" worldSpaceNormal = normal;\n" +
"\n" +
" // calculate position and transform to homogeneous clip space\n" +
" vec4 pos
= vec4(gl_Vertex.x, height * heightScale, gl_Vertex.z, 1.0);\n" +
" gl_Position
= gl_ModelViewProjectionMatrix * pos;\n" +
"\n" +
"eyeSpacePos = (gl_ModelViewMatrix * pos).xyz;\n" +
"eyeSpaceNormal = (gl_NormalMatrix * normal).xyz;\n" +
"}\n";
String fragmentShaderSource = "varying vec3 eyeSpacePos;\n" +
"varying vec3 worldSpaceNormal;\n" +
"varying vec3 eyeSpaceNormal;\n" +
"" +
"uniform vec4 deepColor;\n" +
"uniform vec4 shallowColor;\n" +
"uniform vec4 skyColor;\n" +
"uniform vec3 lightDir;\n" +
"\n" +
"void main()\n" +
"{" +
" vec3 eyeVector
= normalize(eyeSpacePos);\n" +
" vec3 eyeSpaceNormalVector = normalize(eyeSpaceNormal);\n" +
" vec3 worldSpaceNormalVector = normalize(worldSpaceNormal);\n" +
" float facing = max(0.0, dot(eyeSpaceNormalVector, -eyeVector));\n" +
" float fresnel = pow(1.0 - facing, 5.0); // Fresnel approximation\n" +
" float diffuse = max(0.0, dot(worldSpaceNormalVector, lightDir)); \n" +
" vec4 waterColor = mix(shallowColor, deepColor, facing);\n" +
" gl_FragColor = waterColor*diffuse + skyColor*fresnel;\n" +
"}\n";
int meshSize = 1024;
int spectrumW = meshSize + 4;
int spectrumH = meshSize + 1;
// simulation parameters
float g = 9.81f; // gravitational constant
float A = 1e-7f; // wave scale factor
float patchSize = 75.0f; // patch size
float windSpeed = 100.0f;
float windDir = (float) (Math.PI / 3.0f);
93
float dirDepend = 0.07f;
}
ssau.knyazev.oceanOcean.java
package ssau.knyazev.ocean;
import static jcuda.driver.CUgraphicsMapResourceFlags.CU_GRAPHICS_MAP_RESOURCE_FLAGS_WRITE_DISCARD;
import static jcuda.driver.JCudaDriver.*;
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.Point;
import java.awt.event.*;
import java.io.*;
import java.nio.*;
import java.util.Random;
import javax.media.opengl.*;
import javax.media.opengl.awt.GLCanvas;
import javax.media.opengl.glu.GLU;
import javax.swing.JFrame;
import javax.swing.SwingUtilities;
import ssau.knyazev.fft.modules.cuda.CudaFFTModule;
import ssau.knyazev.ocean.OceanConst;
import jcuda.Pointer;
import jcuda.Sizeof;
import jcuda.driver.*;
import jcuda.jcufft.JCufft;
import jcuda.jcufft.cufftHandle;
import jcuda.jcufft.cufftType;
import com.jogamp.opengl.util.Animator;
public class Ocean implements GLEventListener, OceanConst {
private JFrame frame = null;
private Animator animator = null;
private GL2 gl = null;
//OpenGL variables
private int shaderProgramID = 0;
private float translateX = 0.0f;
private float translateY = 0.0f;
private float translateZ = -2.0f;
private float rotateX = 20f;
private float rotateY = 0.0f;
//OpenGL & Cuda variables
private int posVertexBuffer = 0;
private int heightVertexBuffer = 0;
private int slopeVertexBuffer = 0;
private int indexBuffer = 0;
private CUgraphicsResource cuda_heightVB_resource = null;
private CUgraphicsResource cuda_slopeVB_resource = null;
// FFT data
private cufftHandle fftPlan = null;
private CUdeviceptr d_h0Ptr = null;
private CUdeviceptr d_htPtr = null;
private CUdeviceptr d_slopePtr = null;
private float[] h_h0 = null;
94
//JCUDA
private CUdevice device = null;
private CUcontext glContext = null;
private CUfunction generateSpectrumKernel = null;
private CUfunction updateHeightmapKernel = null;
private CUfunction calculateSlopeKernel = null;
private CUmodule module = null;
private float animTime = 0;
private static long sum = 0;
private static int cur = 1;
private static int count = 50;
public Ocean(GLCapabilities capabilities) {
GLCanvas glComponent = new GLCanvas(capabilities);
glComponent.setFocusable(true);
glComponent.addGLEventListener(this);
MouseControl mouseControl = new MouseControl();
glComponent.addMouseMotionListener(mouseControl);
glComponent.addMouseWheelListener(mouseControl);
frame = new JFrame("WaterSurface");
frame.addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
animator.stop();
try{
cuMemFree(d_h0Ptr);
cuMemFree(d_htPtr);
cuMemFree(d_slopePtr);
JCufft.cufftDestroy(fftPlan);
gl.glDeleteBuffers(1, IntBuffer.wrap(new int[] {posVertexBuffer}));
gl.glDeleteBuffers(1, IntBuffer.wrap(new int[] {heightVertexBuffer}));
gl.glDeleteBuffers(1, IntBuffer.wrap(new int[] {slopeVertexBuffer}));
} catch (Exception e1){
e1.printStackTrace();
}
System.exit(0);
}
});
frame.setLayout(new BorderLayout());
glComponent.setPreferredSize(new Dimension(800, 800));
frame.add(glComponent, BorderLayout.CENTER);
frame.pack();
frame.setVisible(true);
glComponent.requestFocus();
// Create and start the animator
animator = new Animator(glComponent);
animator.start();
}
public static void main(String args[]) {
GLProfile profile = GLProfile.get(GLProfile.GL2);
final GLCapabilities capabilities = new GLCapabilities(profile);
SwingUtilities.invokeLater(new Runnable() {
public void run() {
new Ocean(capabilities);
}
});
}
@Override
95
public void init(GLAutoDrawable drawable) {
gl = drawable.getGL().getGL2();
gl.setSwapInterval(0);
gl.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
gl.glEnable(GL2.GL_DEPTH_TEST);
initShaders(gl);
initJCuda();
initVBO(gl);
}
private void initJCuda(){
setExceptionsEnabled(true);
cuInit(0);
device = new CUdevice();
cuDeviceGet(device, 0);
glContext = new CUcontext();
cuGLCtxCreate(glContext, CUctx_flags.CU_CTX_BLOCKING_SYNC, device);
String ptxFileName = "";
try {
ptxFileName = preparePtxFile("resources/ocean.cu");
} catch (IOException e) {
System.err.println("Could not create PTX file");
throw new RuntimeException("Could not create PTX file", e);
}
module = new CUmodule();
cuModuleLoad(module, ptxFileName);
generateSpectrumKernel = new CUfunction();
cuModuleGetFunction(generateSpectrumKernel, module, "generateSpectrumKernel");
updateHeightmapKernel = new CUfunction();
cuModuleGetFunction(updateHeightmapKernel, module, "updateHeightmapKernel");
calculateSlopeKernel = new CUfunction();
cuModuleGetFunction(calculateSlopeKernel, module, "calculateSlopeKernel");
fftPlan = new cufftHandle();
JCufft.cufftPlan2d(fftPlan, meshSize, meshSize, cufftType.CUFFT_C2C);
int spectrumSize = spectrumW * spectrumH * 2;
h_h0 = new float[spectrumSize];
h_h0 = generate_h0(h_h0);
d_h0Ptr = new CUdeviceptr();
cuMemAlloc(d_h0Ptr, h_h0.length*Sizeof.FLOAT);
cuMemcpyHtoD(d_h0Ptr, Pointer.to(h_h0), h_h0.length*Sizeof.FLOAT);
int outputSize = meshSize*meshSize*Sizeof.FLOAT*2;
d_htPtr = new CUdeviceptr();
d_slopePtr = new CUdeviceptr();
cuMemAlloc(d_htPtr, outputSize);
cuMemAlloc(d_slopePtr, outputSize);
}
private void initVBO(GL2 gl) {
int[] buffer = new int[1];
int size = meshSize*meshSize*Sizeof.FLOAT;
gl.glGenBuffers(1, IntBuffer.wrap(buffer));
heightVertexBuffer = buffer[0];
gl.glBindBuffer(GL2.GL_ARRAY_BUFFER, heightVertexBuffer);
gl.glBufferData(GL2.GL_ARRAY_BUFFER, size, (Buffer) null, GL2.GL_DYNAMIC_DRAW);
gl.glBindBuffer(GL2.GL_ARRAY_BUFFER, 0);
cuda_heightVB_resource = new CUgraphicsResource();
cuGraphicsGLRegisterBuffer(cuda_heightVB_resource, heightVertexBuffer,
CU_GRAPHICS_MAP_RESOURCE_FLAGS_WRITE_DISCARD);
buffer = new int[1];
size = meshSize*meshSize*Sizeof.FLOAT*2;
gl.glGenBuffers(1, IntBuffer.wrap(buffer));
slopeVertexBuffer = buffer[0];
96
gl.glBindBuffer(GL2.GL_ARRAY_BUFFER, slopeVertexBuffer);
gl.glBufferData(GL2.GL_ARRAY_BUFFER, size, (Buffer) null, GL2.GL_DYNAMIC_DRAW);
gl.glBindBuffer(GL2.GL_ARRAY_BUFFER, 0);
cuda_slopeVB_resource = new CUgraphicsResource();
cuGraphicsGLRegisterBuffer(cuda_slopeVB_resource, slopeVertexBuffer,
CU_GRAPHICS_MAP_RESOURCE_FLAGS_WRITE_DISCARD);
buffer = new int[1];
size = meshSize*meshSize*Sizeof.FLOAT*4;
gl.glGenBuffers(1, IntBuffer.wrap(buffer));
posVertexBuffer = buffer[0];
gl.glBindBuffer(GL2.GL_ARRAY_BUFFER, posVertexBuffer);
gl.glBufferData(GL2.GL_ARRAY_BUFFER, size, (Buffer) null, GL2.GL_DYNAMIC_DRAW);
gl.glBindBuffer(GL2.GL_ARRAY_BUFFER, 0);
gl.glBindBuffer(GL2.GL_ARRAY_BUFFER, posVertexBuffer);
ByteBuffer byteBuffer = gl.glMapBuffer(GL2.GL_ARRAY_BUFFER, GL2.GL_WRITE_ONLY);
if (byteBuffer != null){
FloatBuffer put = byteBuffer.asFloatBuffer();
int index = 0;
for (int y = 0; y < meshSize; y++) {
for (int x = 0; x < meshSize; x++) {
float u = x / (float) (meshSize - 1);
float v = y / (float) (meshSize - 1);
put.put(index, u * 2.0f - 1.0f);
put.put(index+1, 0.0f);
put.put(index + 2, v * 2.0f - 1.0f);
put.put(index+3, 1.0f);
index += 4;
}
}
}
gl.glUnmapBuffer(GL2.GL_ARRAY_BUFFER);
gl.glBindBuffer(GL2.GL_ARRAY_BUFFER, 0);
size = ((meshSize*2)+2)*(meshSize-1)*Sizeof.INT;
buffer = new int[1];
gl.glGenBuffers(1, IntBuffer.wrap(buffer));
indexBuffer = buffer[0];
gl.glBindBuffer(GL2.GL_ELEMENT_ARRAY_BUFFER, indexBuffer);
gl.glBufferData(GL2.GL_ELEMENT_ARRAY_BUFFER, size, (Buffer) null, GL2.GL_STATIC_DRAW);
byteBuffer = gl.glMapBuffer(GL2.GL_ELEMENT_ARRAY_BUFFER, GL2.GL_WRITE_ONLY);
if (byteBuffer != null){
IntBuffer indices = byteBuffer.asIntBuffer();
int index = 0;
for(int y=0; y<meshSize-1; y++) {
for(int x=0; x<meshSize; x++) {
indices.put(index, y*meshSize+x);
indices.put(index+1, (y+1)*meshSize+x);
index +=2;
}
indices.put(index, (y+1)*meshSize+(meshSize-1));
indices.put(index+1, (y+1)*meshSize);
index += 2;
}
}
gl.glUnmapBuffer(GL2.GL_ELEMENT_ARRAY_BUFFER);
gl.glBindBuffer(GL2.GL_ELEMENT_ARRAY_BUFFER, 0);
}
@Override
public void display(GLAutoDrawable drawable) {
float delta = System.nanoTime();
long time = System.nanoTime();
gl = drawable.getGL().getGL2();
runCuda();
gl.glClear(GL2.GL_COLOR_BUFFER_BIT | GL2.GL_DEPTH_BUFFER_BIT);
97
gl.glMatrixMode(GL2.GL_MODELVIEW);
gl.glLoadIdentity();
gl.glTranslatef(translateX, translateY, translateZ);
gl.glRotatef(rotateX, 1.0f, 0.0f, 0.0f);
gl.glRotatef(rotateY, 0.0f, 1.0f, 0.0f);
gl.glBindBuffer(GL2.GL_ARRAY_BUFFER, posVertexBuffer);
gl.glVertexPointer(4, GL2.GL_FLOAT, 0, 0);
gl.glEnableClientState(GL2.GL_VERTEX_ARRAY);
gl.glBindBuffer(GL2.GL_ARRAY_BUFFER, heightVertexBuffer);
gl.glClientActiveTexture(GL2.GL_TEXTURE0);
gl.glTexCoordPointer(1, GL2.GL_FLOAT, 0, 0);
gl.glEnableClientState(GL2.GL_TEXTURE_COORD_ARRAY);
gl.glBindBuffer(GL2.GL_ARRAY_BUFFER, slopeVertexBuffer);
gl.glClientActiveTexture(GL2.GL_TEXTURE1);
gl.glTexCoordPointer(2, GL2.GL_FLOAT, 0, 0);
gl.glEnableClientState(GL2.GL_TEXTURE_COORD_ARRAY);
gl.glUseProgram(shaderProgramID);
int uniHeightScale = gl.glGetUniformLocation(shaderProgramID, "heightScale");
gl.glUniform1f(uniHeightScale, 0.5f);
int uniChopiness = gl.glGetUniformLocation(shaderProgramID, "chopiness");
gl.glUniform1f(uniChopiness, 1.0f);
int uniSize = gl.glGetUniformLocation(shaderProgramID, "size");
gl.glUniform2f(uniSize, (float) meshSize, (float) meshSize);
int uniDeepColor = gl.glGetUniformLocation(shaderProgramID, "deepColor");
gl.glUniform4f(uniDeepColor, 0.0f, 0.1f, 0.4f, 1.0f);
int uniShallowColor = gl.glGetUniformLocation(shaderProgramID, "shallowColor");
gl.glUniform4f(uniShallowColor, 0.1f, 0.3f, 0.3f, 1.0f);
int uniSkyColor = gl.glGetUniformLocation(shaderProgramID, "skyColor");
gl.glUniform4f(uniSkyColor, 1.0f, 1.0f, 1.0f, 1.0f);
int uniLightDir = gl.glGetUniformLocation(shaderProgramID, "lightDir");
gl.glUniform3f(uniLightDir, 0.0f, 1.0f, 0.0f);
gl.glColor3f(1.0f, 1.0f, 1.0f);
gl.glBindBuffer(GL2.GL_ELEMENT_ARRAY_BUFFER, indexBuffer);
gl.glPolygonMode(GL2.GL_FRONT_AND_BACK, GL2.GL_FILL);
gl.glDrawElements(GL2.GL_TRIANGLE_STRIP, ((meshSize * 2) + 2) * (meshSize - 1),
GL2.GL_UNSIGNED_INT, 0);
gl.glPolygonMode(GL2.GL_FRONT_AND_BACK, GL2.GL_FILL);
gl.glBindBuffer(GL2.GL_ELEMENT_ARRAY_BUFFER, 0);
gl.glDisableClientState(GL2.GL_VERTEX_ARRAY);
gl.glClientActiveTexture(GL2.GL_TEXTURE0);
gl.glDisableClientState(GL2.GL_TEXTURE_COORD_ARRAY);
gl.glClientActiveTexture(GL2.GL_TEXTURE1);
gl.glDisableClientState(GL2.GL_TEXTURE_COORD_ARRAY);
gl.glUseProgram(0);
drawable.swapBuffers();
time = System.nanoTime() - time;
sum +=time;
cur++;
if (cur == count){
time = sum/count;
System.out.println("DISPLAY TIME = " + time);
}
98
delta = System.nanoTime() - delta;
animTime += delta/1000000000;
}
@Override
public void dispose(GLAutoDrawable drawable) { }
@Override
public void reshape(GLAutoDrawable drawable, int x, int y, int width, int height) {
gl = drawable.getGL().getGL2();
GLU glu = GLU.createGLU(gl);
gl.glViewport(0, 0, width, height);
gl.glMatrixMode(GL2.GL_PROJECTION);
gl.glLoadIdentity();
glu.gluPerspective(60.0, (double) width / (double) height, 0.1, 10.0);
}
private void runCuda() {
Pointer kernelParameters = null;
int blockX = 8;
int blockY = 8;
int gridX = meshSize/blockX;
int gridY = gridX;
kernelParameters = Pointer.to(
Pointer.to(d_h0Ptr),
Pointer.to(d_htPtr),
Pointer.to(new int[] { spectrumW }),
Pointer.to(new int[] { meshSize }),
Pointer.to(new int[] { meshSize }),
Pointer.to(new float[] { animTime }),
Pointer.to(new float[] { patchSize }));
cuLaunchKernel(generateSpectrumKernel,
gridX, gridY, 1, // Grid dimension
blockX, blockY, 1, // Block dimension
0, null, // Shared memory size and stream
kernelParameters, null // Kernel- and extra parameters
);
cuCtxSynchronize();
CudaFFTModule.createInstance().fft(d_htPtr, meshSize);
CUdeviceptr g_hptr = new CUdeviceptr();
cuGraphicsMapResources(1, new CUgraphicsResource[]{cuda_heightVB_resource}, null);
cuGraphicsResourceGetMappedPointer(g_hptr, new long[1], cuda_heightVB_resource);
kernelParameters = Pointer.to(
Pointer.to(g_hptr),
Pointer.to(d_htPtr),
Pointer.to(new int[] { meshSize }));
cuLaunchKernel(updateHeightmapKernel,
gridX, gridY, 1, // Grid dimension
blockX, blockY, 1, // Block dimension
0, null, // Shared memory size and stream
kernelParameters, null // Kernel- and extra parameters
);
cuCtxSynchronize();
cuGraphicsUnmapResources(1, new CUgraphicsResource[]{cuda_heightVB_resource}, null);
CUdeviceptr g_sptr = new CUdeviceptr();
cuGraphicsMapResources(1, new CUgraphicsResource[]{cuda_slopeVB_resource}, null);
cuGraphicsResourceGetMappedPointer(g_sptr, new long[1], cuda_slopeVB_resource);
kernelParameters = Pointer.to(
Pointer.to(g_hptr),
Pointer.to(g_sptr),
99
Pointer.to(new int[] { meshSize }),
Pointer.to(new int[] { meshSize }));
cuLaunchKernel(calculateSlopeKernel,
gridX, gridY, 1, // Grid dimension
blockX, blockY, 1, // Block dimension
0, null, // Shared memory size and stream
kernelParameters, null // Kernel- and extra parameters
);
cuCtxSynchronize();
cuGraphicsUnmapResources(1, new CUgraphicsResource[]{ cuda_slopeVB_resource}, null);
}
private void initShaders(GL2 gl) {
shaderProgramID = gl.glCreateProgram();
attachShader(gl, GL2.GL_VERTEX_SHADER, vertexShaderSource);
attachShader(gl, GL2.GL_FRAGMENT_SHADER, fragmentShaderSource);
gl.glLinkProgram(shaderProgramID);
int[] buffer = new int[1];
gl.glGetProgramiv(shaderProgramID, GL2.GL_LINK_STATUS, IntBuffer.wrap(buffer));
gl.glValidateProgram(shaderProgramID);
}
private int attachShader(GL2 gl, int type, String shaderSource){
int shader = 0;
shader = gl.glCreateShader(type);
gl.glShaderSource(shader, 1, new String[] { shaderSource }, null);
gl.glCompileShader(shader);
int[] buffer = new int[1];
gl.glGetShaderiv(shader, GL2.GL_COMPILE_STATUS, IntBuffer.wrap(buffer));
gl.glAttachShader(shaderProgramID, shader);
gl.glDeleteShader(shader);
return shader;
}
// Phillips spectrum
// (Kx, Ky) - normalized wave vector
// Vdir - wind angle in radians
// V - wind speed
// A - constant
private float phillips(float Kx, float Ky, float Vdir, float V, float A, float dir_depend) {
float k_squared = Kx*Kx + Ky*Ky;
if (k_squared == 0.0f)
return 0.0f;
float L = V*V/g;
float k_x = (float) (Kx / Math.sqrt(k_squared));
float k_y = (float) (Ky / Math.sqrt(k_squared));
float w_dot_k = (float) (k_x*Math.cos(Vdir) + k_y*Math.sin(Vdir));
float phillips = (float) (A*Math.exp(-1/(k_squared*L*L))/(k_squared * k_squared) * w_dot_k*w_dot_k);
// filter out waves moving opposite to wind
if (w_dot_k < 0.0f)
phillips *= dir_depend;
return phillips;
}
private float[] generate_h0(float[] h0) {
Random rnd = new Random();
for (int y = 0; y <= meshSize; y++) {
for (int x = 0; x <= meshSize; x++) {
float kx = (float) ((-meshSize/2 + x) * (2*Math.PI / patchSize));
float ky = (float) ((-meshSize/2 + y) * (2*Math.PI / patchSize));
float P = (float) (Math.sqrt(phillips(kx, ky, windDir, windSpeed, A, dirDepend)));
100
if (kx == 0.0f && ky == 0.0f){
P = 0.0f;
}
float Er = (float) rnd.nextGaussian();
float Ei = (float) rnd.nextGaussian();
float h0_re = (float) (Er*P * Math.sqrt(2));
float h0_im = (float) (Ei*P * Math.sqrt(2));
int i = y * spectrumW + x;
h0[2*i] = h0_re;
h0[2*i + 1] = h0_im;
}
}
return h0;
}
class MouseControl implements MouseMotionListener, MouseWheelListener {
private Point previousMousePosition = new Point();
@Override
public void mouseDragged(MouseEvent e) {
int dx = e.getX() - previousMousePosition.x;
int dy = e.getY() - previousMousePosition.y;
if ((e.getModifiersEx() & MouseEvent.BUTTON1_DOWN_MASK) ==
MouseEvent.BUTTON1_DOWN_MASK) {
translateX += dx / 100.0f;
translateY -= dy / 100.0f;
} else if ((e.getModifiersEx() & MouseEvent.BUTTON3_DOWN_MASK) ==
MouseEvent.BUTTON3_DOWN_MASK) {
rotateX += dy;
rotateY += dx;
}
previousMousePosition = e.getPoint();
}
@Override
public void mouseMoved(MouseEvent e) {
previousMousePosition = e.getPoint();
}
@Override
public void mouseWheelMoved(MouseWheelEvent e) {
translateZ += e.getWheelRotation() * 0.25f;
previousMousePosition = e.getPoint();
}
}
private static String preparePtxFile(String cuFileName) throws IOException {
int endIndex = cuFileName.lastIndexOf('.');
if (endIndex == -1) {
endIndex = cuFileName.length() - 1;
}
String ptxFileName = cuFileName.substring(0, endIndex + 1) + "ptx";
File cuFile = new File(cuFileName);
if (!cuFile.exists()) {
throw new IOException("Input file not found: " + cuFileName);
}
String modelString = "-m" + System.getProperty("sun.arch.data.model");
String command = "nvcc " + modelString + " -ptx " + cuFile.getPath() + " -o " + ptxFileName;
System.out.println("Executing\n" + command);
Process process = Runtime.getRuntime().exec(command);
String errorMessage = new String(toByteArray(process.getErrorStream()));
String outputMessage = new String(toByteArray(process.getInputStream()));
int exitValue = 0;
101
try {
exitValue = process.waitFor();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IOException("Interrupted while waiting for nvcc output", e);
}
if (exitValue != 0) {
System.out.println("nvcc process exitValue " + exitValue);
System.out.println("errorMessage:\n" + errorMessage);
System.out.println("outputMessage:\n" + outputMessage);
throw new IOException("Could not create .ptx file: " + errorMessage);
}
System.out.println("Finished creating PTX file");
return ptxFileName;
}
private static byte[] toByteArray(InputStream inputStream) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte buffer[] = new byte[8192];
while (true) {
int read = inputStream.read(buffer);
if (read == -1) {
break;
}
baos.write(buffer, 0, read);
}
return baos.toByteArray();
}
}
ssau.knyazev.fft.modules.cuda.CudaFFTModule.java
package ssau.knyazev.fft.modules.cuda;
import jcuda.Pointer;
import jcuda.Sizeof;
import jcuda.driver.CUcontext;
import jcuda.driver.CUctx_flags;
import jcuda.driver.CUdevice;
import jcuda.driver.CUdeviceptr;
import jcuda.driver.CUfunction;
import jcuda.driver.CUmodule;
import jcuda.driver.JCudaDriver;
import ssau.knyazev.common.CudaCRuntimeException;
import ssau.knyazev.fft.common.FFTConst;
import ssau.knyazev.modules.CudaCompiler;
import ssau.knyazev.modules.Generator;
import ssau.knyazev.modules.Printer;
public class CudaFFTModule {
private static CudaFFTModule instance = null;
private static long sum = 0;
private static int count = 10;
private CUdevice device = null;
private CUcontext context = null;
private CUfunction fftFunction = null;
private CUfunction wlistFunction = null;
private CUfunction invRowsFunction = null;
private CUfunction invColsFunction = null;
private CUfunction inverseFunction = null;
private CUfunction fftRowsFunction = null;
private CUfunction fftColsFunction = null;
private int blockSize = 0;
private int gridSize = 0;
102
private CudaFFTModule() throws CudaCRuntimeException{
JCudaDriver.setExceptionsEnabled(true);
JCudaDriver.cuInit(0);
device = new CUdevice();
JCudaDriver.cuDeviceGet(device, 0);
context = new CUcontext();
JCudaDriver.cuCtxCreate(context, CUctx_flags.CU_CTX_BLOCKING_SYNC, device);
String moduleName = CudaCompiler.preparePtxFile(FFTConst.CUDA_SIMPLE_MODULE_SRC, false);
CUmodule module = new CUmodule();
JCudaDriver.cuModuleLoad(module, moduleName);
fftFunction = new CUfunction();
JCudaDriver.cuModuleGetFunction(fftFunction, module, FFTConst.CUDA_FFT_FUNCTION_NAME);
wlistFunction = new CUfunction();
JCudaDriver.cuModuleGetFunction(wlistFunction, module, FFTConst.CUDA_WLIST_FUNCTION_NAME);
invColsFunction = new CUfunction();
JCudaDriver.cuModuleGetFunction(invColsFunction, module, "inverseCols");
invRowsFunction = new CUfunction();
JCudaDriver.cuModuleGetFunction(invRowsFunction, module, "inverseRows");
fftRowsFunction = new CUfunction();
JCudaDriver.cuModuleGetFunction(fftRowsFunction, module, "fftRows");
fftColsFunction = new CUfunction();
JCudaDriver.cuModuleGetFunction(fftColsFunction, module, "fftCols");
inverseFunction = new CUfunction();
JCudaDriver.cuModuleGetFunction(inverseFunction, module, "inverse");
}
public static CudaFFTModule createInstance(){
if (instance != null){
return instance;
} else {
try {
instance = new CudaFFTModule();
} catch (CudaCRuntimeException e) {
e.printStackTrace();
}
return instance;
}
}
public void fft(CUdeviceptr srcPtr, int N){
blockSize = 16;
gridSize = (N > blockSize) ? N/blockSize : 1;
CUdeviceptr wlistPtr = new CUdeviceptr();
JCudaDriver.cuMemAlloc(wlistPtr, N*2 * Sizeof.FLOAT);
//START FFT
int size = 0;
for (int x = N; x > 1; x = x >> 1){
size++;
}
inverse(N, size - 1, srcPtr);
calculateWList(N, wlistPtr);
for (int iteration = 0; iteration < size; iteration++){
Pointer kernelParameters = Pointer.to(
Pointer.to(new int[] { N }),
Pointer.to(srcPtr),
Pointer.to(wlistPtr),
Pointer.to(new int[] { iteration })
);
JCudaDriver.cuLaunchKernel(fftFunction,
gridSize, gridSize, 1,
blockSize, blockSize, 1,
0, null,
kernelParameters, null
);
JCudaDriver.cuCtxSynchronize();
}
103
//FINISH FFT
JCudaDriver.cuMemFree(wlistPtr);
}
public float[][] fft(float[][] src) throws CudaCRuntimeException{
if (src != null && src.length == src[0].length){
src = transformToComplexMatrix(src);
}
blockSize = 16;
gridSize = (src.length > blockSize) ? src.length/blockSize : 1;
float[] arr = toVector(src);
CUdeviceptr wlistPtr = new CUdeviceptr();
JCudaDriver.cuMemAlloc(wlistPtr, src[0].length * Sizeof.FLOAT);
CUdeviceptr srcPtr = new CUdeviceptr();
JCudaDriver.cuMemAlloc(srcPtr, arr.length * Sizeof.FLOAT);
JCudaDriver.cuMemcpyHtoD(srcPtr, Pointer.to(arr), arr.length * Sizeof.FLOAT);
//START FFT
long time = System.nanoTime();
int size = 0;
for (int x = src.length; x > 1; x = x >> 1){
size++;
}
inverse(src.length, size - 1, srcPtr);
calculateWList(src.length, wlistPtr);
for (int iteration = 0; iteration < size; iteration++){
Pointer kernelParameters = Pointer.to(
Pointer.to(new int[] { src.length }),
Pointer.to(srcPtr),
Pointer.to(wlistPtr),
Pointer.to(new int[] { iteration })
);
JCudaDriver.cuLaunchKernel(fftFunction,
gridSize, gridSize, 1,
blockSize, blockSize, 1,
0, null,
kernelParameters, null
);
JCudaDriver.cuCtxSynchronize();
}
time = System.nanoTime() - time;
sum += time;
//FINISH FFT
JCudaDriver.cuMemcpyDtoH(Pointer.to(arr), srcPtr, arr.length * Sizeof.FLOAT);
src = toMatrix(arr, src.length);
//Printer.printVector(src);
JCudaDriver.cuMemFree(wlistPtr);
JCudaDriver.cuMemFree(srcPtr);
return src;
}
public float[][] fftRowColMode(float[][] src) throws CudaCRuntimeException{
if (src != null && src.length == src[0].length){
src = transformToComplexMatrix(src);
}
blockSize = 16;
gridSize = (src.length > blockSize) ? src.length/blockSize : 1;
float[] arr = toVector(src);
CUdeviceptr wlistPtr = new CUdeviceptr();
JCudaDriver.cuMemAlloc(wlistPtr, src[0].length * Sizeof.FLOAT);
CUdeviceptr srcPtr = new CUdeviceptr();
JCudaDriver.cuMemAlloc(srcPtr, arr.length * Sizeof.FLOAT);
JCudaDriver.cuMemcpyHtoD(srcPtr, Pointer.to(arr), arr.length * Sizeof.FLOAT);
//START FFT
long time = System.nanoTime();
104
int size = 0;
for (int x = src.length; x > 1; x = x >> 1){
size++;
}
inverse(src.length, size-1, srcPtr);
calculateWList(src.length, wlistPtr);
Pointer kernelParameters = Pointer.to(
Pointer.to(new int[] { src.length }),
Pointer.to(srcPtr),
Pointer.to(wlistPtr)
);
JCudaDriver.cuLaunchKernel(fftRowsFunction,
gridSize, gridSize, 1,
blockSize, blockSize, 1,
0, null,
kernelParameters, null
);
JCudaDriver.cuCtxSynchronize();
kernelParameters = Pointer.to(
Pointer.to(new int[] { src.length }),
Pointer.to(srcPtr),
Pointer.to(wlistPtr)
);
JCudaDriver.cuLaunchKernel(fftColsFunction,
gridSize, gridSize, 1,
blockSize, blockSize, 1,
0, null,
kernelParameters, null
);
JCudaDriver.cuCtxSynchronize();
time = System.nanoTime() - time;
sum += time;
//FINISH FFT
JCudaDriver.cuMemcpyDtoH(Pointer.to(arr), srcPtr, arr.length * Sizeof.FLOAT);
src = toMatrix(arr, src.length);
JCudaDriver.cuMemFree(wlistPtr);
JCudaDriver.cuMemFree(srcPtr);
return src;
}
private void inverse(int length, CUdeviceptr srcPtr){
Pointer kernelParameters = Pointer.to(
Pointer.to(new int[] { length}),
Pointer.to(srcPtr)
);
JCudaDriver.cuLaunchKernel(invRowsFunction,
8, 8, 1,
8, 8, 1,
0, null,
kernelParameters, null
);
JCudaDriver.cuCtxSynchronize();
kernelParameters = Pointer.to(
Pointer.to(new int[] { length}),
Pointer.to(srcPtr)
);
JCudaDriver.cuLaunchKernel(invColsFunction,
8, 8, 1,
8, 8, 1,
0, null,
kernelParameters, null
);
JCudaDriver.cuCtxSynchronize();
}
private void inverse(int length, int power, CUdeviceptr srcPtr){
105
Pointer kernelParameters = Pointer.to(
Pointer.to(new int[] { length}),
Pointer.to(new int[] { power}),
Pointer.to(srcPtr)
);
JCudaDriver.cuLaunchKernel(inverseFunction,
gridSize, gridSize, 1,
blockSize, blockSize, 1,
0, null,
kernelParameters, null
);
JCudaDriver.cuCtxSynchronize();
}
private void calculateWList(int length, CUdeviceptr wlistPtr){
Pointer kernelParameters = Pointer.to(
Pointer.to(new int[] { length }),
Pointer.to(wlistPtr)
);
JCudaDriver.cuLaunchKernel(wlistFunction,
gridSize, gridSize, 1,
blockSize, blockSize, 1,
0, null,
kernelParameters, null
);
JCudaDriver.cuCtxSynchronize();
}
protected static float[] toVector(float[][] src) {
float[] res = new float[src.length * src[0].length];
int k = 0;
for (int i = 0; i < src.length; i++) {
for (int j = 0; j < src[0].length; j++) {
res[k] = src[i][j];
k++;
}
}
return res;
}
protected static float[][] toMatrix(float[] src, int rows) {
float[][] res = new float[rows][src.length / rows];
int k = 0;
for (int i = 0; i < rows; i++) {
for (int j = 0; j < src.length / rows; j++) {
res[i][j] = src[k];
k++;
}
}
return res;
}
protected static float[][] transformToComplexMatrix(float[][] src){
float[][] res = new float[src.length][src[0].length*2];
for (int i = 0; i < src.length; i++){
for (int j = 0; j < src.length; j++){
res[i][j*2] = src[i][j];
}
}
return res;
}
public static void main(String[] args) {
float[][] src = Generator.generateSimpleMatrix(2048);
try {
CudaFFTModule core = createInstance();
for (int i = 0; i < count; i++){
src = core.fft(src);
}
106
} catch (CudaCRuntimeException e) {
e.printStackTrace();
}
long time = sum/count;
System.out.println(time);
}
}
resources/fft-c-module.cu
__device__
float4 ccomplex_new()
{
return make_float4(0, 0, 0, 0);
}
__device__
float4 to_ccomplex(float2 arg)
{
return make_float4(arg.x, arg.y, 0, 0);
}
__device__
float2 to_complex(float4 arg)
{
return make_float2(arg.x - arg.w, arg.y + arg.z);
}
__device__
float4 ccomplex_expi(float arg)
{
return make_float4(cosf(arg), sinf(arg), 0, 0);
}
__device__
float4 ccomplex_expj(float arg)
{
return make_float4(cosf(arg), 0, sinf(arg), 0);
}
__device__
float2 conjugate(float2 arg)
{
return make_float2(arg.x, -arg.y);
}
__device__
float4 ccomplex_add(float4 a, float4 b)
{
return make_float4(a.x + b.x, a.y + b.y, a.z + b.z, a.w + b.w);
}
__device__
float4 ccomplex_sub(float4 a, float4 b){
return make_float4(a.x - b.x, a.y - b.y, a.z - b.z, a.w - b.w);
}
__device__
int mirror_index(int srcIndex, int matrixWidth)
{
int matrixIndex = srcIndex/matrixWidth * matrixWidth;
return (matrixIndex == srcIndex) ? srcIndex : matrixWidth - srcIndex + 2*matrixIndex;
}
__device__
void mirror(float4* src, int size, int down, int right, int dmirr, int rmirr)
{
int downright = size*down + right;
107
if (rmirr != right){
int downrmirr = down*size + rmirr;
src[downrmirr].x = src[downright].x;
src[downrmirr].y = src[downright].y;
src[downrmirr].z = -src[downright].z;
src[downrmirr].w = -src[downright].w;
}
if (dmirr != down){
int dmirrright = dmirr*size + right;
src[dmirrright].x = src[downright].x;
src[dmirrright].y = -src[downright].y;
src[dmirrright].z = src[downright].z;
src[dmirrright].w = -src[downright].w;
}
if (rmirr != right && dmirr != down){
int dmirrrmirr = dmirr*size + rmirr;
src[dmirrrmirr].x = src[downright].x;
src[dmirrrmirr].y = -src[downright].y;
src[dmirrrmirr].z = -src[downright].z;
src[dmirrrmirr].w = src[downright].w;
}
}
__device__
float4 ccomplex_multy(float4 a, float4 b){
return make_float4(
a.x * b.x - a.y * b.y - a.z * b.z + a.w * b.w,
a.x * b.y + a.y * b.x - a.z * b.w - a.w * b.z,
a.x * b.z - a.y * b.w + a.z * b.x - a.w * b.y,
a.x * b.w + a.y * b.z + a.z * b.y + a.w * b.x);
}
__device__ __constant__ float _PI = 3.141592653;
extern "C"
__global__ void fft(int size, float4* a, float4* wilist, float4* wjlist, int iteration){
int idx = blockIdx.x * gridDim.x + threadIdx.x;
int idy = blockIdx.y * gridDim.y + threadIdx.y;
if (idx < size && idy < size){
int step = 1 << iteration;
int step2 = step*2;
bool flag = (idy/step) % 2 + (idx/step) % 2 > 0;
if (!flag){
float4 w1 = wilist[(idy % step2)*(size/step2)];
float4 w2 = wjlist[(idx % step2)*(size/step2)];
int a00i = idy*size + idx;
int a10i = (idy+step)*size + idx;
int a01i = idy*size + (idx+step);
int a11i = (idy+step)*size + (idx+step);
float4 a00 = a[a00i];
float4 a10 = a[a10i];
float4 a01 = a[a01i];
float4 a11 = a[a11i];
float4 newa00 = ccomplex_new();
float4 newa01 = ccomplex_new();
float4 newa10 = ccomplex_new();
float4 newa11 = ccomplex_new();
newa00 = ccomplex_add(newa00, a00);
newa10 = ccomplex_add(newa10, a00);
newa01 = ccomplex_add(newa01, a00);
newa11 = ccomplex_add(newa11, a00);
float4 t = ccomplex_multy(w1, a10);
newa00 = ccomplex_add(newa00, t);
newa10 = ccomplex_sub(newa10, t);
newa01 = ccomplex_add(newa01, t);
newa11 = ccomplex_sub(newa11, t);
108
t = ccomplex_multy(w2, a01);
newa00 = ccomplex_add(newa00, t);
newa10 = ccomplex_add(newa10, t);
newa01 = ccomplex_sub(newa01, t);
newa11 = ccomplex_sub(newa11, t);
t = ccomplex_multy(w2, a11);
t = ccomplex_multy(w1, t);
newa00 = ccomplex_add(newa00, t);
newa10 = ccomplex_sub(newa10, t);
newa01 = ccomplex_sub(newa01, t);
newa11 = ccomplex_add(newa11, t);
a[a00i] = newa00;
a[a10i] = newa10;
a[a01i] = newa01;
a[a11i] = newa11;
}
}
__syncthreads();
}
extern "C"
__global__ void fftMirror(int size, float4* a, float4* wilist, float4* wjlist, int iteration){
int idx = blockIdx.x * gridDim.x + threadIdx.x;
int idy = blockIdx.y * gridDim.y + threadIdx.y;
if (idx < size && idy < size){
int step = 1 << iteration;
int matrixWidth = step << 1;
int part = matrixWidth/4;
int dmatrInd = idy/matrixWidth * matrixWidth;
int rmatrInd = idx/matrixWidth * matrixWidth;
int tdown = idy - dmatrInd;
int tright = idx - rmatrInd;
bool flag = (tdown <= part && tright <= part);
if (flag){
float4 w1 = wilist[(idy % matrixWidth)*(size/matrixWidth)];
float4 w2 = wjlist[(idx % matrixWidth)*(size/matrixWidth)];
int a00i = idy*size + idx;
int a10i = (idy+step)*size + idx;
int a01i = idy*size + (idx+step);
int a11i = (idy+step)*size + (idx+step);
float4 a00 = a[a00i];
float4 a10 = a[a10i];
float4 a01 = a[a01i];
float4 a11 = a[a11i];
float4 newa00 = ccomplex_new();
float4 newa01 = ccomplex_new();
float4 newa10 = ccomplex_new();
float4 newa11 = ccomplex_new();
newa00 = ccomplex_add(newa00, a00);
newa10 = ccomplex_add(newa10, a00);
newa01 = ccomplex_add(newa01, a00);
newa11 = ccomplex_add(newa11, a00);
float4 t = ccomplex_multy(w1, a10);
newa00 = ccomplex_add(newa00, t);
newa10 = ccomplex_sub(newa10, t);
newa01 = ccomplex_add(newa01, t);
newa11 = ccomplex_sub(newa11, t);
t = ccomplex_multy(w2, a01);
newa00 = ccomplex_add(newa00, t);
newa10 = ccomplex_add(newa10, t);
newa01 = ccomplex_sub(newa01, t);
109
newa11 = ccomplex_sub(newa11, t);
t = ccomplex_multy(w2, a11);
t = ccomplex_multy(w1, t);
newa00 = ccomplex_add(newa00, t);
newa10 = ccomplex_sub(newa10, t);
newa01 = ccomplex_sub(newa01, t);
newa11 = ccomplex_add(newa11, t);
a[a00i] = newa00;
a[a10i] = newa10;
a[a01i] = newa01;
a[a11i] = newa11;
if (iteration >= 2){
int rmirr = mirror_index(idx, matrixWidth);
int dmirr = mirror_index(idy, matrixWidth);
mirror(a, size, idy, idx, dmirr, rmirr);
rmirr = mirror_index(idx + step, matrixWidth);
mirror(a, size, idy, idx + step, dmirr, rmirr);
dmirr = mirror_index(idy + step, matrixWidth);
rmirr = mirror_index(idx, matrixWidth);
mirror(a, size, idy + step, idx, dmirr, rmirr);
rmirr = mirror_index(idx + step, matrixWidth);
mirror(a, size, idy + step, idx + step, dmirr, rmirr);
}
}
}
__syncthreads();
}
extern "C"
__global__ void wlist(int size, float4* wi, float4* wj){
int blockSize = blockDim.x * blockDim.y;
int block = blockIdx.y * gridDim.x + blockIdx.x;
int idx = (blockSize * block) + threadIdx.y * blockDim.x + threadIdx.x;
if (idx < size){
wi[idx] = ccomplex_expi(-2*_PI*idx/size);
wj[idx] = ccomplex_expj(-2*_PI*idx/size);
}
}
extern "C"
__global__ void inverseRows(int size, float4* src){
int blockSize = blockDim.x * blockDim.y;
int block = blockIdx.y * gridDim.x + blockIdx.x;
int idx = (blockSize * block) + threadIdx.y * blockDim.x + threadIdx.x;
int col = idx;
if (idx < size){
int icol = 0;
int jcol = 0;
int dl = size/2;
int st = size - 1;
int j = 0;
for (int i = 0; i < st; i++){
if (i < j){
icol = i*size + col;
jcol = j*size + col;
float4 s0 = src[icol];
src[icol] = src[jcol];
src[jcol] = s0;
}
int k = dl;
while (k <= j){
j = j - k;
110
k = k >> 1;
}
j = j + k;
}
}
}
extern "C"
__global__ void inverseCols(int size, float4* src){
int blockSize = blockDim.x * blockDim.y;
int block = blockIdx.y * gridDim.x + blockIdx.x;
int idx = (blockSize * block) + threadIdx.y * blockDim.x + threadIdx.x;
int row = idx;
if (idx < size){
int rowi = 0;
int rowj = 0;
int dl = size/2;
int st = size - 1;
int j = 0;
for (int i = 0; i < st; i++){
if (i < j){
rowi = row*size + i;
rowj = row*size + j;
float4 s0 = src[rowi];
src[rowi] = src[rowj];
src[rowj] = s0;
}
int k = dl;
while (k <= j){
j = j - k;
k = k >> 1;
}
j = j + k;
}
}
}
extern "C"
__global__ void inverse(int size, int power, float4* src){
int idx = blockIdx.x * gridDim.x + threadIdx.x;
int idy = blockIdx.y * gridDim.y + threadIdx.y;
if (idx < size && idy < size){
int indexy = idy;
int indexx = idx;
int newrow = 0;
int newcol = 0;
for (int l = power; l >= 0; l--){
int k = 1 << l;
if (k <= indexy){
indexy -= k;
int delta = 1 << (power-l);
newrow += delta;
}
if (k <= indexx){
indexx -= k;
int delta = 1 << (power-l);
newcol += delta;
}
}
int rowcol = idy*size + idx;
int newrownewcol = newrow*size + newcol;
if (idy > newrow){
float4 t = src[rowcol];
src[rowcol] = src[newrownewcol];
src[newrownewcol] = t;
} else if (idy == newrow && idx > newcol){
float4 t = src[rowcol];
src[rowcol] = src[newrownewcol];
111
src[newrownewcol] = t;
}
}
}
extern "C"
__global__ void convertToCComplex(int size, float2* src, float4* out){
int idx = blockIdx.x * gridDim.x + threadIdx.x;
int idy = blockIdx.y * gridDim.y + threadIdx.y;
if (idx < size && idy < size){
int uid = idy*size + idx;
out[uid] = to_ccomplex(src[uid]);
}
}
extern "C"
__global__ void convertToComplex(int size, float2* src, float4* out){
int idx = blockIdx.x * gridDim.x + threadIdx.x;
int idy = blockIdx.y * gridDim.y + threadIdx.y;
if (idx < size && idy < size){
int uid = idy*size + idx;
src[uid] = to_complex(out[uid]);
}
}
resources/ocean.cu
///////////////////////////////////////////////////////////////////////////////
#include <cufft.h>
#include <math_constants.h>
//Round a / b to nearest higher integer value
int cuda_iDivUp(int a, int b)
{
return (a + (b - 1)) / b;
}
// complex math functions
__device__
float2 conjugate(float2 arg)
{
return make_float2(arg.x, -arg.y);
}
__device__
float2 complex_exp(float arg)
{
return make_float2(cosf(arg), sinf(arg));
}
__device__
float2 complex_add(float2 a, float2 b)
{
return make_float2(a.x + b.x, a.y + b.y);
}
__device__
float2 complex_mult(float2 ab, float2 cd)
{
return make_float2(ab.x * cd.x - ab.y * cd.y, ab.x * cd.y + ab.y * cd.x);
}
// generate wave heightfield at time t based on initial heightfield and dispersion relationship
extern "C"
__global__ void generateSpectrumKernel(float2* h0,
float2 *ht,
unsigned int in_width,
unsigned int out_width,
unsigned int out_height,
112
float t,
float patchSize)
{
unsigned int x = blockIdx.x*blockDim.x + threadIdx.x;
unsigned int y = blockIdx.y*blockDim.y + threadIdx.y;
unsigned int in_index = y*in_width+x;
unsigned int in_mindex = (out_height - y)*in_width + (out_width - x); // mirrored
unsigned int out_index = y*out_width+x;
// calculate wave vector
float2 k;
k.x = (-(int)out_width / 2.0f + x) * (2.0f * CUDART_PI_F / patchSize);
k.y = (-(int)out_width / 2.0f + y) * (2.0f * CUDART_PI_F / patchSize);
// calculate dispersion w(k)
float k_len = sqrtf(k.x*k.x + k.y*k.y);
float w = sqrtf(9.81f * k_len);
if ((x < out_width) && (y < out_height)) {
float2 h0_k = h0[in_index];
float2 h0_mk = h0[in_mindex];
// output frequency-space complex values
ht[out_index] = complex_add( complex_mult(h0_k, complex_exp(w * t)), complex_mult(conjugate(h0_mk),
complex_exp(-w * t)) );
//ht[out_index] = h0_k;
}
}
// update height map values based on output of FFT
extern "C"
__global__ void updateHeightmapKernel(float* heightMap,
float2* ht,
unsigned int width)
{
unsigned int x = blockIdx.x*blockDim.x + threadIdx.x;
unsigned int y = blockIdx.y*blockDim.y + threadIdx.y;
unsigned int i = y*width+x;
// cos(pi * (m1 + m2))
float sign_correction = ((x + y) & 0x01) ? -1.0f : 1.0f;
heightMap[i] = ht[i].x * sign_correction;
}
// generate slope by partial differences in spatial domain
extern "C"
__global__ void calculateSlopeKernel(float* h, float2 *slopeOut, unsigned int width, unsigned int height)
{
unsigned int x = blockIdx.x*blockDim.x + threadIdx.x;
unsigned int y = blockIdx.y*blockDim.y + threadIdx.y;
unsigned int i = y*width+x;
float2 slope = make_float2(0.0f, 0.0f);
if ((x > 0) && (y > 0) && (x < width-1) && (y < height-1)) {
slope.x = h[i+1] - h[i-1];
slope.y = h[i+width] - h[i-width];
}
slopeOut[i] = slope;
}
113
Download