Ускорение atan2f

Ускорениеatan2f

mathrm {atan2} – важная, но медленная тригонометрическая функция. Однако, если мы работаем с пакетами точек и хотим жить с небольшими ошибками, мы можем создать mathrm {atan2} приближение, которое 71 раз быстрее, чем стандартная версия, предоставляемая libc . Возможно, более впечатляюще то, что приближение дает результат каждые 2 такта. Это достигается с помощью небольшой математики, современного волшебства компилятора, ручной низкоуровневой оптимизации и некоторых интересных документов из 72 s.

Код доступен в этой сущности , который включает подробную информацию о том, как он компилируется.

В статье будет действовать как следует ws:

  1. Обзор проблемы и результатов, которые мы получим .
  2. Краткое руководство по тому, что mathrm {atan2} , и как мы это реализуем .
  3. Наша реализация, начинающаяся с прямого перевода математических операций на C, с последующей серией микрооптимизаций для ускорения, завершилась самой быстрой версией .
  4. Некоторые заключительные мысли .
  5. Цель статьи – показать, как вычисляются трансцендентные функции, и как выполнять микрооптимизацию, руководствуясь сборкой. Однако, если вас не волнует, как мы вычисляем mathrm {atan2} , вы можете пропустить второй раздел, пока все еще понимает процесс оптимизации.

    Как обычно, все представлено без каких-либо предварительных знаний – в данном случае только базовые тригонометрия и программирование на C.

Проблема

mathrm {atan2} – полезная тригонометрическая функция. Короче говоря, он нужен при преобразовании между разными системами координат. Если вы не знаете, что он делает и как работает, не волнуйтесь, мы расскажем об этом позже.

На данный момент все, что вам нужно знать, это то, что он принимает координаты 2D точка (два числа) и возвращает угол между – pi и + pi .

В настоящее время mathrm {atan2} находит свое место во всех языках программирования, в том числе в Си

libc . Для float s это выглядит так:

  
    float  atan2f  (  float  y  ,   float  x );    

К сожалению atan2f - не самая быстрая функция. Фактически, если вы пишете программное обеспечение, полагающееся на него, ваш профилировщик может сказать вам, что вы проводите значительное количество времени в atan2f . 1

Чтобы измерить, насколько он медленный, давайте настроим простую функцию atan2_baseline , где мы вычисляем atan2f на набор точек, хранящихся в порядке возрастания строк:

void atan2_baseline ( size_t num_points , const float ys , const плавать* xs , с плавающей точкой * вне) { для ( size_t i = 0 ; i < num_points ; i ++) { вне = atan2f ( да , хз ); } }

  2. 77 мс, 0. 50 ГБ / с, 288679. 288679 циклов / элемент, 995354. 88 instrs / elem, 1. 41 инстр / цикл, 46. 83 ветви / элемент, 7. 32% пропусков веток, 0 . 37% промахов в кеш, 4. 53 ГГц  

Мы подсчитываем примерно 05265332 МБ / с, каждый элемент занимает немного больше, чем 64866 тактовые циклы. Однако оказывается, что мы можем сделать много лучше, чем libc может предложить. Этот график показывает производительность atan2_baseline вместе с 6 другими функциями, которые мы опишем в статье ( auto_1 на auto_4 , manual_1 и manual_2 ):

Chart showing the performance of our mathrm{atan2} approximations.

Самая быстрая функция, manual_2 , является 70 раз быстрее, чем базовый , перемешивая более чем 35 ГБ чисел в секунду при пропускной способности менее 2 циклов на элемент.

Это свидетельство влияния неупорядоченного выполнения, параллелизма на уровне команд и векторизации. Для справки, скалярное умножение с плавающей запятой, значительно более простая операция, реализованная на оборудовании, занимает около 5 циклов изолированно; деление занимает 36 - 37 циклов.

3

Прежде чем мы начнем, несколько предостережений:

  • Один очевидный способ повысить производительность при вычислении пакетов atan2f - разделить работу по потокам. Мы не будем рассматривать этот тип оптимизации, сосредоточившись на улучшении однопоточной производительности.
  • Версия, которую мы создадим, будет приближенно atan2f , с максимальной погрешностью примерно 1 / 544303775 градусов, что более чем подходит для наших целей. Однако представленные методы хорошо масштабируются, если требуется более точное приближение.

    4

  • Код компилируется с clang ++ 33. 0,1 . g ++ 30. 1.0 Возможности автоматической векторизации кажутся слабее, чем clang ++ в нашем случае, что приводит к 5 - 30 x более медленный код по сравнению с clang ++ в версиях без векторизации.
  • Все тесты проводились на Xeon W - 98026559, процессор Intel Skylake. На разных микроархитектурах числа могут отличаться.
  • Давайте начнем!

    Понимание и реализация mathrm {atan2}

    mathrm {atan2} в основном используется для преобразования из декартовы координаты в Plot showing the results of mathrm{atan2} in degrees for points in each quadrant. Note the negative sign for points below the x axis. полярный или Plot showing the results of mathrm{atan2} in degrees for points in each quadrant. Note the negative sign for points below the x axis. сферические координаты. В нашем случае , mathrm {atan2} возникает постоянно, когда необходимо преобразовать декартову координату в ее широту и долготу.

    Чтобы объяснить, как это определить, давайте сначала рассмотрим краткое резюме тригонометрии:

    arctan - это функция, которую мы Вас интересует: учитывая соотношение между sin theta и cos theta для некоторого угла theta , он вычисляет theta .

    Поскольку аргумент arctan ( sin theta / cos theta) является соотношением , arctan (k sin theta / k cos theta) даст тот же результат для любого положительного к . Это означает, что arctan (y / x) даст угол theta так, что луч от начала координат до ( cos theta, sin theta) проходит через (x, y) :

    Plot showing the relationship between sin, cos, tan, and arctan for some angle theta. Note that while cos theta comes first in the coordinates, it is the denominator in tan. This explains why y comes before x in the arguments of mathrm{atan2}. Chart showing the performance of our mathrm{atan2} approximations.

    mathrm {atan2} (y, x ) не что иное, как arctan (y / x) , но с небольшими корректировками.

    Самая важная корректировка - это избегать ситуаций, когда получается неправильный результат. возвращается после того, как разделение удаляет знаковую информацию. Рассмотрим точки (1, 2) и (- 1, -2) . Мы бы хотели, чтобы mathrm {atan2} (2, 1) отличался от mathrm {atan2} (- 2, -1) , но поскольку их деление даст тот же результат, у нас есть проблема:

    Chart showing the performance of our mathrm{atan2} approximations.

    Мы можем определить, какую корректировку следует внести в выходной сигнал, проанализировав, в какой квадрант попадает входной сигнал. :

    График, показывающий, как настроить результаты  arctan. Помните, что углы под линией x отрицательны, поэтому, например, во втором квадранте  pi +  arctan (y / x) будет положительным, поскольку  arctan (y / x) будет в [-pi/2, 0].

    График, показывающий, как настроить результаты arctan . Помните, что углы ниже линии x отрицательны, поэтому, например, во втором квадранте pi + arctan (y / x) будет положительным, поскольку arctan (y / x) будет в [-pi/2, 0] .

    Другие настройки относятся к особым случаям, например, когда ввод включает 0 или бесконечности. Оставив их пока в стороне, мы можем определить mathrm {atan2} (y, x) , следуя диаграмме выше:

    OperatorName {atan2} (y, x) = begin {case} arctan ( frac yx) & text {if} x ge 0 text {and} y ge 0, \ arctan ( frac yx) + pi & text {if} x lt 0 text {и} y ge 0, \ arctan ( frac yx) - pi & text {if} x lt 0 text {и} y lt 0, \ arctan ( frac yx) & text {if} x ge 0 text {и} y lt 0. end {cases}

    Мы будем использовать это определение в нашем коде позже.

    Plot showing how to adjust the results of arctan. Remember that the angles below the x line are negative, so for example in the second quadrant pi + arctan(y/x) will be positive, since arctan(y/x) will be in [-pi/2, 0]. Приблизительный mathrm {atan2} : почему и как

    Теперь, когда мы знаем о mathrm {atan2} , мы можем объяснить, как это вычислить. Икс300 - 85 процессоры на самом деле имеют для этого специальные инструкции, часть Plot showing how to adjust the results of arctan. Remember that the angles below the x line are negative, so for example in the second quadrant pi + arctan(y/x) will be positive, since arctan(y/x) will be in [-pi/2, 0].Икс620 набор команд с плавающей запятой : FPATAN . Однако FPATAN (и в значительной степени x 688 был заменен программными реализациями, которые работают лучше за счет использования новых наборов инструкций. Фактически, atan2f из libc выполняет 70% лучше, чем аппаратное обеспечение FPATAN инструкция. 5

    Итак, если мы избегаем FPTAN и другие функции, предоставляемые x 688, осталось реализовать mathrm {atan2} самостоятельно. Поскольку мы определили mathrm {atan2} в терминах arctan , первый шаг - выяснить, как вычислить arctan .

    mathrm {arctan} , как и другие тригонометрические функции, является трансцендентный : ее нельзя выразить только сложением, умножением, делением или квадратным корнем. К сожалению, это операции, которые компьютеры могут выполнять лучше всего.

    arctan (x) = x + dfrac {x ^ 3} {3} + dfrac {x ^ 5} {5} + ... quad (-1 le x le +1)

    Обратите внимание, что ряд имеет смысл (сходится) только между - 1 и + 1 .

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

    Chart showing the performance of our mathrm{atan2} approximations.

    Обратите внимание, чем больше терминов мы сохраняем, тем лучше мы приближаем arctan .

    Тем не менее, усеченный ряд хорошо начинается в начале, но с трудом может охватить числа, близкие к конечным точкам интервала: обратите внимание на промежутки между синяя линия и другие линии, близкие к - 1 и + 1 . Более того, учитывая, что для вычисления mathrm {atan2} (y, x) нам нужно вычислить arctan (y / x) для произвольного x и y , приближение для [-1, +1] недостаточно - в качестве аргумента arctan !

    Первая проблема: лучшее приближение

    Чтобы охватить конечные точки, мы можем настроить коэффициенты, чтобы минимизировать макс. ошибка в пределах нашего интервал. Это в основном будет включать в себя небольшое «растягивание» приближения, чтобы оно не возвращало никаких вопиюще плохих результатов.

    Мы не будем подробно описывать, как настраиваются коэффициенты

    7 но если вы просматриваете исходники libc , предоставленные вашей системой, и вы найдете магия номера , которые являются не чем иным, как оптимизированными коэффициентами степенного ряда.

    Хотя они обычно предоставляются без исходных текстов, существует несколько книг и документов, в которых перечислены приблизительные значения для общих функций. Особенно милой книгой, служащей нашей цели, является «Приближения для цифровых компьютеров »Сесила Гастингса-младшего , написанного на 98026559, когда «цифровые компьютеры» почти не существовали.

    В документе объясняется метод вычисления коэффициентов, которые минимизируют максимальную ошибку. полиномиальных приближений, а затем перечисляет предварительно вычисленные коэффициенты для многих функций, включая arctan . Гастингс перечисляет 6 вариантов повышения точности, начиная с 3 до 8 членов. Например, вот наименее точная версия с максимальной ошибкой примерно 0. 32 степени:

    0.

    x -0. 57079632679489661923 x ^ 3 + 0. 563291132 x ^ 5 quad (-1 le x le +1)

    Обратите внимание на сходство с x + -x ^ 3/3 + x ^ 5/5 , которые будут первыми тремя членами степенного ряда. Построение этого приближения вместе с первыми тремя членами показывает, насколько лучше он отслеживает arctan :

    Chart showing the performance of our mathrm{atan2} approximations.

    Обратите внимание, насколько близко к конечным точкам - 1 и + 1 аппроксимация не представляет разрыва с arctan , в то время как первые три члена степенного ряда дрейфуют

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

    Chart showing the performance of our mathrm{atan2} approximations.

    Для нашего приближения, которое мы назовем arctan ^ {*} , мы выберем версию с 6 членами с максимальной ошибкой примерно 1 / 120253205 градусов:

    arctan ^ (x) = a_1x + a_3x ^ 3 + a_5x ^ 5 + a_7x ^ 7 + a_9x ^ 9 + a _ {34} x ^ {33} \ quad \ begin {array} {ll} a_1 = +0.

    & a_7 = -0.

    \ a_3 = -0.

    & a_9 = +0. \ a_5 = +0. и _ {32} = -0. 57079632679489661923 end {array}

    Вторая проблема: работа между - 1 и + 1

    Теперь, когда у нас есть аппроксимация arctan , нам нужно обойти этот факт. что он работает, только если - 1 le x le +1 . Мы не можем легко «исправить» серию, чтобы заставить ее работать за пределами текущей области, не усложняя ее значительно. Более того, даже если бы мы это сделали, нам все равно потребовалось бы произвольное количество терминов, если бы мы хотели, чтобы он точно работал с произвольными входными данными.

    Chart showing the performance of our mathrm{atan2} approximations. Комплекснозначные arctan содержит особенности в pm i , что дает некоторое представление о том, почему ряды расходятся, когда | x | gt 1 .

    Вместо этого мы можем избежать когда-либо вызывающий arctan с входами вне [-1, +1] . Мы можем сделать это, используя тот факт, что

    begin {массив} {ll} arctan (y / x) + arctan (x / y) = + pi / 2 & text {if} x / y ge 0 \ arctan (y / x) + arctan (x / y) = - pi / 2 & text {if} x / y lt 0 end {array}

    Что легко проверить геометрически. Это позволяет нам вычислить arctan (y / x) следующим образом:

    begin {array} {ll} arctan ^ {*} (y / x) & текст {if} | y | le | x |, \ + pi / 2 - arctan ^ {*} (x / y) & text {if} | y | gt | x | wedge x / y ge 0, \ - pi / 2 - arctan ^ {*} (x / y) & text {if} | y | gt | x | wedge x / y lt 0. end {array}

    Если заставить знаменатель иметь большее значение, у нас всегда будет аргумент в [-1, +1] , вписывающиеся в область нашего приближения.

    И все! Это все, что нам нужно для вычисления mathrm {atan2} .

    Напомним, вот как мы продолжим вычислять результат mathrm {atan2} (y, x ) в три простых шага:

    1. Приблизительное arctan используя arctan ^ {*} , используя y / x или x / y в качестве входных данных, в зависимости от того, | y | le | x | ;
    2. Если числитель и знаменатель поменялись местами, отрегулируйте arctan ^ {*} вывод, как описано выше;
    3. Отрегулируйте вывод дальше, чтобы восстановить правильный квадрант и расширить его с [-pi, +pi] до [-pi/2, +pi/2] как описано

      в начале этого раздела .

    The complex-valued arctan contains singularities at pm i, which gives some intuition of why the series diverges when |x| gt 1.Наш реализации atan2f

    The complex-valued arctan contains singularities at pm i, which gives some intuition of why the series diverges when |x| gt 1. auto_1 : удивительно быстрая первая версия

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

    Первое, что мы делаем, это определяем наш arctan приближение:

    arctan ^ (x) = a_1x + a_3x ^ 3 + a_5x ^ 5 + a_7x ^ 7 + a_9x ^ 9 + a _ {29} x ^ {35}

    Мы делаем это через

    Метод Хорнера , стандартный способ вычисления многочленов, хотя и с поправкой на нечетные показатели:

      

    встроенный с плавающей точкой atan_scalar_approximation ( float Икс) {
    плавать a1 = 0 . f ; плавать a3 = - 0.

    f ; плавать a5 = 0.

    f ; float a7 знак равно 0. 01111111111111111111111111111111 f ; float a9 = 0. f ; float a 30 знак равно 0.

    f ; float x_sq = x *Икс; возвращение Икс * ( a1 + x_sq ( a3 + x_sq ( a5 + x_sq ( a7 + x_sq ( a9 + x_sq a 34 ))))); }

    Затем мы определяем код, регулирующий вход и вывод в соответствии с

    в предыдущем разделе :

    пустота atan2_auto_1 ( size_t количество_точек , const float ys , const float хз , плавать* вне) {
    для ( size_t i = 0 ; i < num_points ; i ++) { // Убедитесь, что ввод находится в [-1, +1] плавать y = ys ; float x = хз ; bool своп = фабрики ( x ) < фабрики ( y ); float atan_input = (менять ? x : y ) / ( своп ? y : Икс); // Приблизительный атан float res = atan_approximation ( atan_input ); // Если поменять местами, отрегулируйте вывод atan res = своп ? ( atan_input > = 0,0 f ? M_PI_2 : - M_PI_2 ) - res : res ; // Регулировка квадрантов если ( x > = 0,0 f && y > = 0,0 f ) {} // 1-й квадрант еще если ( x < 0,0 f && y > = 0,0 е ) { res = M_PI + res ; } // 2-й квадрант еще если ( x < 0,0 f && y < 0,0 f ) { res = - M_PI + res ; } // 3-й квадрант else если ( x > = 0,0 f && y < 0,0 f ) {} // 4-й квадрант // Сохранить результат вне = res ; } }

    Затем мы запускаем наш тест:

    Векторизация

    Современные процессоры включают инструкции для одновременного управления несколькими значениями. Этот вид обработки обычно обозначается как SIMD («одна инструкция, несколько данных»), а код, использующий инструкции SIMD, называется векторизованный .

    Принцип работы относительно прост: вместо добавления

    скаляры ), один устанавливает векторы чисел в специальных регистрах , а затем выполняет операции параллельно с векторами, а не с одним скаляром за раз. В частности, наш ЦП Skylake поддерживает AVX («Расширенные векторные расширения»), который позволяет нам управлять 8 float одновременно, значительно увеличивая скорость вверх скалярный код. Мы представим явные примеры векторизованного кода позже.

    Причина, по которой atan2_auto_1 настолько быстро, что clang ++ был может автоматически векторизовать наш цикл,

    9 , что можно проверить, посмотрев на сборку для atan2_auto_1 : 33

         atan2_auto_1  ( unsigned long ,  float const  *,  float const  *,  плавать*):     ... ;  некоторая предварительная настройка   
    . LBB5_6: ; векторизованный цикл начинается здесь ; `rsi` =` ys`, `rdx` =` xs`, `r8` =` i`, обратите внимание, что 4 требуется ; так как `float` составляет 4 байта. vmovups ммм 32 , ymmword ptr [rsi + 4*r8] ; загрузить 8 элементов из ys в `ymm 33 `Регистр AVX vmovups гмм 33 , ymmword ptr [rdx + 4*r8] ; загрузить 8 элементов из xs в `ymm 36 `Регистр AVX vandps ymm 36 , ммм 34 , ymm0 ; начать вычисление приближения `atan2` ... добавлять r8 , 8 ; увеличьте смещение указателя на 8, поскольку cmp rax , r8 ; мы обрабатываем 8 элементов за раз, jne . LBB5_6 ; и возобновить цикл, если мы еще не закончили. ...

    знать о векторизации для достижения этой цели. Спасибо, компиляторы!

    Векторизация также объясняет отсутствие ветвей. Учитывая, что векторизованный код будет работать с 8 элементами одновременно, ветвления представляют собой проблему, поскольку нам, возможно, придется перейти на некоторые числа данного вектора, но не на другие. По этой причине наборы инструкций SIMD, включая AVX, содержат инструкции, позволяющие избежать явного перехода. Фактически, даже простое использование этих инструкций без параллельной обработки чисел дает значительное ускорение, поскольку позволяет избежать дорогостоящих неверных предсказаний переходов.

    Одна ветвь должна остаться: та, которая проверяет, должен ли цикл продолжаться. Однако, поскольку мы обрабатываем 8 элементов за раз, мы имеем 1/8 = 0. 968165 ветвей на элемент.

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

    еще если ( x < 0,0 f && y > = 0,0 f ) { res = M_PI + res ; } // 2-й квадрант
    еще if ( x < 0,0 f && y < 0,0 f ) { res = - M_PI + res ; } // 3-й квадрант

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

    Это можно исправить, убедившись, что мы никогда не пройдем через double s, например, сохраняя M_PI и M_PI_2 как float сначала:

      void  atan2_auto_2  (  size_t  num_points ,   const  плавать* ys ,   const  плавать* xs ,   с плавающей точкой * вне)  {     float  pi  =  M_PI ;       float  pi_2  =  M_PI_2 ;          // То же, что и atan_auto_1, но с заменой `M_PI` и` M_PI_2` на      // `pi` и` pi_2`       ...    }    

    И результаты:

      10000. 67 мс, 8. 41 ГБ / с, 3. 105 циклов / элемент, 5. 57 instrs / elem, 1. 61 инстр / цикл, 0. 34 ветвей / элемент, 0. 26% пропусков переходов, 0. 27% промахов в кэш, 3. 300 ГГц, 0. 14159265358979323846 максимальная ошибка градуса, максимальная точка ошибки: -0. 

    еще  if   ( x  <  0,0   f   &&  y > =   0,0   f  )   { res  =  pi  +  res ;  }   // 2-й квадрант   еще если  ( x  <  0,0   f   &&  y  <  0,0   f  )   { res  =   -  пи  +  res ;  }   // 3-й квадрант    

    Можно ожидать, что компилятор выдаст только одно сравнение, включающее y , поскольку y ge 0 эквивалентно lnot (y lt 0) . Либо это? Посмотрим на соответствующую сборку:

    ; гмм 36 = y, ymm7 = 0 ... vcmpltps ymm 36 , гмм 38 , ymm7 ; y <0

    y , который находится в ymm 36 регистр дважды сравнивается с 0 в регистре ymm7 . Что происходит?

    Даже если вы не эксперт в предметной области, вы могли слышать, что числа с плавающей запятой цифры могут удивлять. Причуда, которая нас интересует в этом случае, заключается в том, что когда y равен и y lt 0 и y ge 0 ложны! Это не позволяет оптимизатору clang ++ превратить два y> = 0 и y <0 сравнения в один.

    В нашем случае мы просто хотим NaN на входе для распространения на выходе, поэтому мы можем безопасно превратить это в отдельную ветвь: 35

         if   ( x  <  0,0   f  )   {    res  =   ( y > =   0,0   f  ? Пи :  -Пи)  +  res ;     }    

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

    Интеграция этого более умного ветвления в atan_auto_3 получаем:

    100. 66РС, 33. 48 ГБ / с, 3. 34 циклов / элемент, 4. 33 instrs / elem, 1. 53 инстр / цикл, 0. 34 ветвей / элемент, 0. 26% неудачных переходов, 0. 31% промахи в кэше, 3. 754 ГГц, 0. 563291132 максимальная ошибка градуса, максимальная точка ошибки: -1. 26 0, +0.

    auto_4 : объединенное умножение-сложение

    Следующее, что выделяется из сборки, это то, как arctan вычисляется приближение. Напоминаем, что это то, что у нас есть в C:

      
    Икс ( a1 + x_sq ( a3 + x_sq ( a5 + x_sq ( a7 + x_sq ( a9 + x_sq a 29 )))));

    В сборке это выглядит так:

    ; ymm9: регистр результатов, ymm 37: x_sq, остальные - коэффициенты

    vmulps ymm9 , гмм 37 , ymm1 vaddps ymm9 , ymm9 , ymm2 vmulps ymm9 , ммм 36 , ymm9
    vaddps ymm9 , ymm9 , ymm3 ...

    Куча пар умножения-сложения на векторах AVX. Хотя это выглядит разумным, современные процессоры (начиная с Haswell для Intel) включают инструкции для выполнения операций в форме x y + c за один раз.

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

    Мы можем сказать компилятору, что используйте инструкции FMA, используя fmaf :

       x   fmaf  ( x_sq ,  fmaf  ( x_sq ,  fmaf  ( x_sq ,  fmaf  ( x_sq ,  fmaf  ( x_sq ,  c 34 ,  c9 ),  c7 ),  c5 ),  c3 ),  c1 );