Зведення числа в дійсну ступінь - delphi sources faq

Як, ніхто цього ще не придумав?
Є пропозиція
Чого ми досягли?
Апроксимація функції 2x
Новий варіант функції зведення в ступінь
Апроксимація функції log2x і "спеціалізація" піднесення до степеня
висновок
джерело мудрості

Як, ніхто цього ще не придумав?

Не беруся судити. Ймовірно, завдання про те, як максимально швидко звести дійсне додатне число в довільну дійсний ступінь, вирішувалася приблизно так само часто, як і вставала, - а вставала, гадаю, не раз. І все ж не так давно я з жахом виявив, що RTL зі складу Borland Delphi останніх версій (як Delphi 6, так і Delphi 7) підходить до вирішення не більше професійно, ніж старанний п'ятикласник, яка вперше зіштовхнулася з такою проблемою.

Погляньмо на вихідний код функції Power з модуля Math, люб'язно наданий Borland Software:

Примітно, що в благих цілях оптимізації процесор залишають наодинці з цілим натовпом розгалужень, що призводять його, врешті-решт, в загальному випадку до горезвісного рішенням п'ятикласника, а саме, до тривіальної формулою

Тут x ** y означає зведення x в ступінь y. a exp (x) = e ** x.

Що поганого в такому підході до вирішення? По-перше, в набір інструкцій FPU не входить ні операція обчислення exp (x), ні взяття натурального логарифма ln (x). Відповідно, результат обчислюється в кілька етапів, в той час як можна піти більш прямим шляхом, як буде показано нижче. За рахунок цього падає швидкість обчислення; крім того, тут діє інтуїтивне правило, яке грубо можна сформулювати так: чим більше операцій виконується над числом з плаваючою комою в регістрах співпроцесора, тим більше буде і сумарна похибка результату.

Пізніша перевірка показала, що як Visual C з Visual Studio .NET, так і C ++ Builder 4.5 реалізують зведення в ступінь більш якісно. Використовуваний в них варіант концептуально не відрізняється від того рішення, яке я хочу запропонувати.

Давайте проведемо інвентаризацію. Які інструкції співпроцесора пов'язані зі зведенням до степеня або взяттям логарифма? Наведу коротку цитату з [1] і [2]:

  • F2XM1 - обчислює 2 ** x-1. де -1
  • FSCALE (масштабування ступенем двійки) - фактично вважає 2 ** trunc (x). де trunc (x) означає округлення до 0, тобто позитивні числа округлюються в меншу сторону, негативні - у велику.
  • FXTRACT - витягує мантиссу і експоненту дійсного числа.
  • FYL2X - обчислює y * log2 (x). де log2 (x) - логарифм x за основою 2.
  • FYL2XP1 - обчислює y * log2 (x + 1) для - (1-1 / sqrt (2))

Ось, загалом-то, і все. Але вже на перший погляд цього вистачає, щоб зрозуміти, що завдання може бути вирішена більш прямо, ніж пропонує RTL Borland Delphi.

Дійсно, чому не замінити показник ступеня в (1) на 2? Адже неперово число аж ніяк не є рідною для двійковій арифметики! тоді вийде

Цей вислів для x ** y відповідно до вищезазначеними п'ятьма інструкціями можна уявити як композицію функцій у такому вигляді:

Так як обчислити f (z) в одну дію неможливо, доводиться вважати так:

Формули (4) - (6) природно виражаються таким асемблерним кодом:

Перед виконанням цього фрагмента коду потрібно переконатися, що біти управління округленням в слові управління FPU встановлені в режим округлення до нуля. У Delphi це найпростіше зробити за допомогою функції SetRoundMode (модуль Math):

Так як на процесорах Intel Pentium IV послідовне багаторазове перемикання між двома (але не більше) станами слова управління FPU виконується набагато швидше, ніж на ранніх моделях, можна розраховувати, що навіть в тих ситуаціях, коли доведеться чергувати виклик цього фрагмента коду з діями, які вимагають іншого режиму округлення, при роботі на сучасній техніці це не призведе до надмірних додаткових тимчасових витратах. Подробиці див. Наприклад, в [3].

Повний код працездатною функції на Object Pascal виглядає так:

Має сенс створити перевантажені версії функції для різних типів аргументів FLOATTYPE, так як на практиці часто головним недоліком вбудованої функції є те, що вона (як і всі викликані нею функції) приймає в якості аргументів дійсні числа типу Extended, що призводить до вельми істотних витрат на конвертування форматів при завантаженні в стек FPU.

На жаль, виграш в швидкості абсолютно не відчувається. Це цілком зрозуміло: згідно з додатком C ( "IA-32 Instruction Latency and Throughput") документа [3], з усього цього фрагмента основна обчислювальна навантаження лягає на трансцендентні (відповідальність за не цілком коректне застосування терміна лягає не на мене, а на панів з Intel) операції, а саме - FYL2X, FRNDINT, F2XM1 і FSCALE. Кількість же цих операцій в запропонованому алгоритмі і їх загальне число в реалізації функцій ln (x) і exp (x) в RTL Delphi однаково.

Звичайно, хотілося б збільшити і швидкість обчислень. Але світ не ідеальний, і за це доведеться розплачуватися все тією ж точністю. Як правило, в кожній ситуації існує межа допустимих похибок при розрахунках. В ілюстративних цілях я задався максимальної допустимої відносної похибкою 0,0001 = 0,1%. Насправді, як буде видно з графіків відносної похибки, вдалося досягти ще більшої точності.

Подальші наші дії повинні полягати в тому, щоб виключити трансцендентні математичні операції. Ясно, що будь-яке уявлення у вигляді кінцевої композиції елементарних арифметичних операцій деякої функції, неразложимой в таку композицію, є наближенням вихідної функції. Тобто завдання ставиться так: потрібно наблизити використовувані трансцендентні функції композиціями елементарних операцій, залишаючись при цьому в допустимих для похибки межах.

Апроксимація функції 2x

Цей захід дозволить нам позбутися відразу і від тривалої F2XM1, і від виконується набагато швидше FSCALE.

Існує безліч способів наблизити функцію f (x). Один з найбільш простих в обчислювальному плані - підбір відповідного по точності многочлена g (x) = an x ​​n + an-1 x n-1 +. + A1 x + a0. Його коефіцієнти можуть бути постійні, а можуть деяким чином залежати від x. У першому випадку коефіцієнти легко знайти методом найменших квадратів, взявши значення вихідної функції в декількох точках і підібравши коефіцієнти так, щоб в цих точках многочлен брав значення, як можна ближчі до значень функції (докладний опис поліноміальною апроксимації функцій і методу найменших квадратів можна знайти в книгах, присвячених курсам обчислювальної математики або обробці експериментальних даних). Простота методу обертається істотним недоліком: він часом непоганий для виявлення якісних тенденцій, але погано відображає вихідну функцію кількісно, ​​причому, як правило, похибка зростає зі зменшенням ступеня многочлена n. а швидкість обчислення g (x) з ростом n падає.

Гідна альтернатива, що дозволяє досить точно наближати гладкі криві, такі, як y = 2 ** x, - апроксимація сплайнами. Говорячи простою мовою (можливо, надто простим - нехай мене вибачать фахівці), сплайн - це крива, що моделює форму, прийняту пружним стержнем, деформованим шляхом закріплення в заданих точках. Вона проходить точно через задані точки, підкоряючись при цьому деяким додатковим умовам, зокрема, умові безперервності другої похідної. Існують різні види сплайнів. У цій роботі досить практично використання кубічних сплайнів. Кубічний сплайн на кожному відрізку між двома послідовними (в порядку зростання координати x) еталонними точками (x, f (x)) описується поліномом третього ступеня g (x) = a3 x 3 + a2 x 2 + a1 x + a0. де набір коефіцієнтів (a0, a1, a2, a3) свій для кожного відрізка. Пошук цих коефіцієнтів - не надто складне завдання, але опис методу її рішення виходить за рамки цієї статті. Таблиця коефіцієнтів. що виходить після обліку всіх зауважень цього розділу, додається до статті.

Отже, я обмежуся лише використанням отриманих мною значень коефіцієнтів. Щоб забезпечити необхідну точність на проміжку 0<=x <999, мне понадобились в качестве эталонных 2039 точек, которым соответствовали значения x=(i-40)/2. i= 0..2038. Сорок значений на отрицательной полуоси нужны были только для того, чтобы отразить поведение сплайна в этой части плоскости, слегка скорректировав таким образом его вид на остальных отрезках; в вычислениях эти 40 отрезков не участвуют, т.к. для значений x <0 можно воспользоваться (без ощутимого проигрыша в скорости или точности) соотношением 2**(-|x|)=1/(2**|x|).

На вхід функції Exp2 надходить єдиний аргумент x - споруджений в ступінь число. Як можна реалізувати функцію?

Ось як це зробив я.

Як і для попередньої функції, потрібно забезпечити установку біт управління округленням в режим округлення до нуля.

Виконання цього фрагмента змінює вміст регістру EAX.

Оцінимо похибка наближення. Так як результат, одержуваний як _Power (2, x) (функція _Power приведена на початку статті), свідомо точніше, ніж Exp2 (x), то в якості оченкі приймемо відносне відхилення значення останньої функції від значення першої: Epsilon = abs (Exp2 ( x) - _Power (2, x)) / _Power (2, x). Зрозуміло, вираз має сенс, якщо _Power (2, x)<>0.


Малюнок 1. Максимальна похибка наближення функції Exp2 = 2 ** x (при x менше 990) не перевищує 0,004%.

Новий варіант функції зведення в ступінь

Змінимо реалізацію зведення в ступінь відповідно до запропонованої аппроксимацией для 2 ** x:

У цьому фрагменті використовується інструкція FCOMIP, що вперше з'явилася на процесорах Pentium Pro. Любителям антикваріату доведеться використовувати замість пари команд FCOMIP / JE блок

А замість FCOMIP / JA - блок

До того ж в цьому випадку змінюється регістр EAX.

Результати тестування відображені на графіках:


Малюнок 2. Тимчасові витрати: New_Power - нова функція, Power - зі складу RTL Borland Delphi.

Підпис X-0.511 на осі абсцис відображає той факт, що при проведенні випробувань бралися значення цілі значення X, до яких потім додавалося число 0.511, щоб гарантувати, що підстава ступеня - число нецілим (тобто щоб розглядати по можливості загальний випадок).

Чорна лінія поверх червоного набору - згладжені тимчасові витрати функції Power, фіолетова поверх синього - функції New_Power.

Заміри витрат часу проводилися за допомогою інструкції RDTSC (процесори починаючи з Pentium):

RDTSC повертає в реєстрової парі EDX: EAX число тактів процесора, що минули з моменту останнього скидання (reset). Машинний код інструкції - 0Fh, 31h.

Відносна похибка поводиться напрочуд стабільно, змінюючись в межах від 0 до 0,0040%. Тому досить представницьким безліччю значень аргументу є, наприклад, проміжок (0, 1000).

Видно, що оцінена відносна похибка (фактично - відхилення від значення, що повертається вбудованої функцією) насправді не перевершує 0.004%!

У разі показника ступеня 17 коливання стають набагато частіше, однак загальна картина та сама.

Апроксимація функції log2x і "спеціалізація" піднесення до степеня

Логарифмування погано піддається апроксимації з допомогою кубічних сплайнів - точніше, мені вдалося це зробити, причому з досить високою точністю, але лише ціною програшу за часом в порівнянні з використанням FYL2X. Однак тут є що робити і не вдаючись до сплайнів.

Як відомо, функція ln (1 + x) при | x |<1 разлагается в ряд Тейлора следующим образом:

Якщо абсолютна величина x досить мала, члени ряду, вже починаючи з третього, досить слабо позначаються на результаті. Тому для значень x. досить близьких до 1 (щоб залишитися в обумовлених вище рамках прийнятних похибок, x повинен відстояти від 1 цієї статті не більше ніж на 0.01), обчислення log2 (x) = ln (x) / ln (2) = ln (x) * log2 (e ) = ln (1 + (x-1)) * log2 (e) можна замінити обчисленням (tt * t / 2) * log2 (e), де t = x-1.

Це дозволяє побудувати ще один варіант функції зведення в ступінь для значень підстави, близьких до 1. У ньому немає інструкції FYL2X, а замість неї присутній блок інструкцій, позначених символом "*" (знак "

"Означає наближене рівність):

Таким чином, нам в цьому випадку (при x, близьких до 1) вдається позбутися від всіх інструкцій FPU, що належать до групи трансцендентних, що призводить до вражаючого зростання продуктивності:


Малюнок 4. Тимчасові витрати: New_Power_XNear1 - спеціалізований варіант New_Power.

На жаль, із зростанням показника ступеня максимальна похибка зростає, залишаючись, втім, в обумовлених межах (тобто менше 0,1%, більше того - менше 0,01%) навіть при дуже високих показниках:

Таким чином, нам вдалося отримати функції, переважаючі вбудовану за швидкістю від двох до чотирьох разів при похибки близько 0.004% - 0.01%. Не виключено, що існує можливість провести більш якісну і більш вигідну в сенсі тимчасових витрат апроксимацію функцій; можливо, навіть за іншим принципом, а не так, як це було зроблено тут (тобто виходячи зі співвідношення x ** y = 2 ** (y * log2 (x))).

Для тих же випадків, коли необхідна висока точність обчислень, як першого каменя фундаменту була розглянута функція, що виправляє недолік Delphi RTL. Безсумнівно, цей напрямок також гідно подальших досліджень з метою прискорити свідомо повільні обчислення з підвищеною точністю.

Дуже пізнавально читання наступних документів:

  1. Intel® Architecture Software Developer's Manual. том 2, Instruction set reference. Можна знайти на сайті Intel www.intel.com.
  2. Intel® VTune ™ Performance Analyzer, гіпертекстова довідка. І взагалі, VTune - чудовий інструмент для пошуку шорсткостей в програмі.
  3. Intel® Pentium® 4 and Intel® Xeon ™ Processor Optimization Reference Manual. Все там же, на сайті Intel.