Számítási feladatokban közvetlenül vagy műveletátalakítások után nem ritkán találkozhatunk azzal, hogy két számot össze kell szorozni és a szorzathoz egy harmadik számot kell adni (x*y+z). Ha ezt a kifejezést float típusú számokkal kell kiértékelni, akkor a véges számábrázolási pontosság miatt két kerekítés történhet. A balról jobbra kiértékelés során az első kerekítés a szorzás eredményére vonatkozik, a második a szorzat és a harmadik szám összeadásának eredményére. Ezért a kerekítési hibák halmozódhatnak, ami a végeredmény pontosságra kedvezőtlen hatással lehet. Ez különösen a nagy pontosságot igénylő számításoknál okozhat gondot.
A kedvezőtlen hatást lehet csökkenteni az úgynevezett összevont szorzás és összeadás (fused multiply-add, FMA) művelet alkalmazásával. Ennek lényege, hogy csak egyetlen kerekítés lesz a végeredményen, mert a közbensőt elkerüli mintha a szorzás végtelen pontossággal történne.
Ez a művelet a decimal modul Decimal osztályában már régóta rendelkezésre áll az fma() metódus formájában. Azonban a Decimal objektumokkal való számolás nem olyan gyakori (némi előképzettséget is igényel) mint a float számokkal való munka. Viszont az FMA művelet float számokra nem volt eddig biztosítva. Ez a helyzet azonban a Python 3.13 verziótól megváltozott, mert a math modulban rendelkezésre áll az fma(x, y, z) függvény. Ezt tehát az (x*y)+z eredményét adja, de egyetlen kerekítéssel.
Alább három függvényt (fn1, fn2, fn3) láthatunk, amelyeket ugyanarra a bemenő számhármasra hívunk meg, hogy összevethessük a számítási eredmények pontosságát. Az első függvény az operátorokkal végzett x*y+z kifejezés eredményét adja vissza. A második a math.fma() függvényt, a harmadik a Decimal.fma() metódust használja úgy, hogy a decimális aritmetika számítási környezetének pontosságát meglehetősen nagyra állítottuk be. Ezzel azt érjük el, hogy a Decimal objektumokat alkalmazó függvény az egzakt eredményt szolgáltatja, ami így referencia lehet a másik kettő pontosságának megítéléséhez.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
from math import fma from decimal import Decimal, getcontext from timeit import timeit import sys assert sys.version_info[:2] >= (3, 13) # A működéshez Python 3.13+ szükséges getcontext().prec = 100 # A decimális aritmetika pontosságának beállítása. def fn1(x, y, z): return x * y + z def fn2(x, y, z): return fma(x, y, z) def fn3(x, y, z): x, y, z = map(str, (x, y, z)) return Decimal(x).fma(Decimal(y), Decimal(z)) # TESZT data = (1.0000008, 987654321, 123456789) print('x = {}, y = {}, z = {}'.format(*data)) for txt, fn in zip(('x * y + z = ', 'fma(x, y, z) = ', 'Decimal(x).fma(y, z) = ', 'normal dec'), (fn1, fn2, fn3)): print('{}\t{:.25f}'.format(txt, fn(*data)).expandtabs(24)) print('Futási idő = {:.3f}'.format(timeit('fn(*data)', globals=globals()))) # Eredmény: # x = 1.0000008, y = 987654321, z = 123456789 # x * y + z = 1111111900.1234569549560546875000000 # Futási idő = 0.086 # fma(x, y, z) = 1111111900.1234567165374755859375000 # Futási idő = 0.096 # Decimal(x).fma(y, z) = 1111111900.1234568000000000000000000 # Futási idő = 1.995 |
A függvénydefiníciók alatt láthatók a tesztsorok és az eredmények, amelyek az egyes számítási módokkal elérhető pontosságot és futási időt mutatják. Látható, hogy az operátorokkal végzett művelet a leggyorsabb, de egyben a legkisebb pontosságot produkálja. Az fma() függvény némi futási idő többlet árán nagyobb pontosságot ad. A Decimal fma() metódussal végrehajtott művelet nagyon nagy pontosságú, de a másik kettőhöz képest jelentős végrehajtási idő árán.
Tehát a valamit-valamiért elv azonban itt is érvényes, mert a nagyobb pontosságért a megnövekedett futási idővel fizetünk.
A FMA művelet általában pontosabb eredményt ad, mint a szorzás és összeadás operátorokkal végzett. De ez valójában csak sok számítás esetén mutatkozik meg, mert a kerekítési hibák olykor csökkenthetik egymást így az operátorokkal való művelet adott esetben akár pontosabb is lehet, mint az FMA. Ennek demonstrálását láthatjuk a következő kódsorokban. Itt a feladat, hogy két iterálható objektum azonos pozícióban levő elemeit szorozzuk össze és adjuk össze a szorzatokat. /Ha az egyes elemeket vektor-koordinátáknak fogjuk fel, akkor a feladat, hogy két vektor skaláris szorzatát képezzük./
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 |
from math import fma, sumprod from decimal import Decimal, getcontext from random import uniform from typing import Iterable import sys assert sys.version_info[:2] >= (3, 13) # A működéshez Python 3.13+ szükséges cntx = getcontext() cntx.prec = 100 # A decimális aritmetika pontosságának beállítása. def sum_of_products(xs: Iterable, ys: Iterable): """Az argumentumok azonos pozícióban levő elemeit összeszorozza és a szorzatok összegével tér vissza.""" # Számítási elv: (((x0*y0+0)+x1*y1)+x2*y2) partial_result = 0 for x, y in zip(xs, ys): partial_result = fma(x, y, partial_result) return partial_result # Teszt a math.fma() függvény alkalmazásával. num_of_experiment = 100000 fma_is_worse = 0 for _ in range(num_of_experiment): a = [uniform(1.00000001, 1.00000009) for _ in range(2)] b = [uniform(1000000000, 9999999999) for _ in range(2)] s = sum(x * y for x, y in zip(a, b)) # float típusú elemek szorzása majd a szorzatok összegzése. s_fma = sum_of_products(a, b) # A math.fma() alapú elemszorzás és összegzés. dec = sum(Decimal(str(x)) * Decimal(str(y)) for x, y in zip(a, b)) # A pontos érték számítása Decimal számokkal. # A pontos értékhez képesti hibák meghatározása a kétféle számítási módra. error_s = abs(Decimal(s) - dec) error_fma = abs(Decimal(s_fma) - dec) if error_s < error_fma: fma_is_worse += 1 print(f'A math.fma() alapú elemszorzás és összegzés az esetek {fma_is_worse / num_of_experiment:.2%}-ban ' f'ad csak rosszabb eredményt.') # Teszt a math.sumprod() függvény alkalmazásával. sumprod_is_worse = 0 for _ in range(num_of_experiment): a = [uniform(1.00000001, 1.00000009) for _ in range(2)] b = [uniform(1000000000, 9999999999) for _ in range(2)] s = sum(x * y for x, y in zip(a, b)) # float típusú elemek szorzása majd a szorzatok összegzése. sp = sumprod(a, b) # A math.sumprod() függvénnyel való elemszorzás és összegzés. dec = sum(Decimal(str(x)) * Decimal(str(y)) for x, y in zip(a, b)) # A pontos érték számítása Decimal számokkal. # A pontos értékhez képesti hibák meghatározása a kétféle számítási módra. error_s = abs(Decimal(s) - dec) error_sumprod = abs(Decimal(sp) - dec) if error_s < error_sumprod: sumprod_is_worse += 1 print(f'A math.sumprod() függvénnyel való elemszorzás és összegzés az esetek {sumprod_is_worse / num_of_experiment:.2%}-ban ' f'ad csak rosszabb eredményt.') # Eredmények: # A math.fma() alapú elemszorzás és összegzés az esetek 6.74%-ban rosszabb eredményt ad. # A math.sumprod() függvénnyel való elemszorzás és összegzés az esetek 6.87%-ban rosszabb eredményt ad. |
A kívánt műveletet a sum_of_products() függvényben a math.fma() függvénnyel valósítottuk meg. Ennek pontosságát hasonlítjuk össze azzal, amikor az elemeket a szorzás operátorral összeszorozzuk és a szorzatokat a sum() beépített függvénnyel összeadjuk. A két iterálható objektum elemeit adott tartományban véletlenszerűen állítjuk elő. Pontossági referenciának a Decimal objektummal számolt értéket vesszük. Azt nézzük, hogy az FMA művelettel megvalósított sum_of_products() függvény mikor ad rosszabb pontosságot, mint az operátor és sum() függvényes megoldás.
Két iterálható objektum azonos pozícióban levő elemeinek szorzására és a szorzatok összegzésére van más nyelvi lehetőség is. Ugyanis a Python 3.12 óta rendelkezésre áll a sumprod() függvény, ami szintén megnövelt pontossággal számol. A tesztet ennek használatával is elvégeztük.
Az eredményekből megállapítható, hogy mind a math.fma(), mind a math.sumprod() alkalmazásával számolva ezek csak az esetek kevesebb mint 7%-ában teljesítenek rosszabbul pontosság szempontból, mint az operátorral és sum() függvénnyel végzett számítás.
E bejegyzés témájához a Python tudásépítés lépésről lépésre című e-könyv „Készétel fogyasztás – a szabványos könyvtár moduljainak használata” fejezetéből a „A programvégrehajtás felfüggesztése és a futási idő mérése”, valamint a „Matematikai számítások támogatása” alfejezet, ezen belül is az „Amikor fontos a pontos számítás – decimal modul” és a „A véletlen használatba vétele” című részek kapcsolódnak.