Összevont szorzás és összeadás (fused multiply-add, FMA) alkalmazása a pontosabb számításokhoz

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.

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./

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.

Érdekel a Python tudásépítés lépésről lépésre az alapoktól az első asztali alkalmazásig című e-könyv.