2013-03-22 15 views
6

(dipendenze per questo programma:. vector --any e JuicyPixels >= 2 codice è disponibile come Gist.)Ottimizzazione Perlin in Haskell

{-# LANGUAGE Haskell2010 #-} 
{-# LANGUAGE BangPatterns #-} 

import Control.Arrow 
import Data.Bits 
import Data.Vector.Unboxed ((!)) 
import Data.Word 
import System.Environment (getArgs) 

import qualified Codec.Picture as P 
import qualified Data.ByteString as B 
import qualified Data.Vector.Unboxed as V 

ho cercato di porto Ken Perlin's improved noise a Haskell, ma non sono del tutto sicuro che il mio il metodo è corretto La parte principale è qualcosa che dovrebbe generalizzare bene per dimensioni superiori e inferiori, ma che è qualcosa per dopo:

perlin3 :: (Ord a, Num a, RealFrac a, V.Unbox a) => Permutation -> (a, a, a) -> a 
perlin3 p (!x', !y', !z') 
    = let (!xX, !x) = actuallyProperFraction x' 
     (!yY, !y) = actuallyProperFraction y' 
     (!zZ, !z) = actuallyProperFraction z' 

     !u = fade x 
     !v = fade y 
     !w = fade z 

     !h = xX 
     !a = next p h + yY 
     !b = next p (h+1) + yY 
     !aa = next p a + zZ 
     !ab = next p (a+1) + zZ 
     !ba = next p b + zZ 
     !bb = next p (b+1) + zZ 
     !aaa = next p aa 
     !aab = next p (aa+1) 
     !aba = next p ab 
     !abb = next p (ab+1) 
     !baa = next p ba 
     !bab = next p (ba+1) 
     !bba = next p bb 
     !bbb = next p (bb+1) 

    in 
     lerp w 
      (lerp v 
       (lerp u 
        (grad aaa (x, y, z)) 
        (grad baa (x-1, y, z))) 
       (lerp u 
        (grad aba (x, y-1, z)) 
        (grad bba (x-1, y-1, z)))) 
      (lerp v 
       (lerp u 
        (grad aab (x, y, z-1)) 
        (grad bab (x-1, y, z-1))) 
       (lerp u 
        (grad abb (x, y-1, z-1)) 
        (grad bbb (x-1, y-1, z-1)))) 

Questa è ovviamente accompagnato da alcune funzioni descritte nella funzione perlin3 , di cui io spero che siano il più efficiente possibile:

fade :: (Ord a, Num a) => a -> a 
fade !t | 0 <= t, t <= 1 = t * t * t * (t * (t * 6 - 15) + 10) 

lerp :: (Ord a, Num a) => a -> a -> a -> a 
lerp !t !a !b | 0 <= t, t <= 1 = a + t * (b - a) 

grad :: (Bits hash, Integral hash, Num a, V.Unbox a) => hash -> (a, a, a) -> a 
grad !hash (!x, !y, !z) = dot3 (vks `V.unsafeIndex` fromIntegral (hash .&. 15)) (x, y, z) 
    where 
    vks = V.fromList 
     [ (1,1,0), (-1,1,0), (1,-1,0), (-1,-1,0) 
     , (1,0,1), (-1,0,1), (1,0,-1), (-1,0,-1) 
     , (0,1,1), (0,-1,1), (0,1,-1), (0,-1,-1) 
     , (1,1,0), (-1,1,0), (0,-1,1), (0,-1,-1) 
     ] 

dot3 :: Num a => (a, a, a) -> (a, a, a) -> a 
dot3 (!x0, !y0, !z0) (!x1, !y1, !z1) = x0 * x1 + y0 * y1 + z0 * z1 

-- Unlike `properFraction`, `actuallyProperFraction` rounds as intended. 
actuallyProperFraction :: (RealFrac a, Integral b) => a -> (b, a) 
actuallyProperFraction x 
    = let (ipart, fpart) = properFraction x 
     r = if x >= 0 then (ipart, fpart) 
         else (ipart-1, 1+fpart) 
    in r 

Per il gruppo di permutazioni, io ho semplicemente copiato quello Perlin utilizzato sul suo sito:

newtype Permutation = Permutation (V.Vector Word8) 

mkPermutation :: [Word8] -> Permutation 
mkPermutation xs 
    | length xs >= 256 
    = Permutation . V.fromList $ xs 

permutation :: Permutation 
permutation = mkPermutation 
    [151,160,137,91,90,15, 
    131,13,201,95,96,53,194,233,7,225,140,36,103,30,69,142,8,99,37,240,21,10,23, 
    190, 6,148,247,120,234,75,0,26,197,62,94,252,219,203,117,35,11,32,57,177,33, 
    88,237,149,56,87,174,20,125,136,171,168, 68,175,74,165,71,134,139,48,27,166, 
    77,146,158,231,83,111,229,122,60,211,133,230,220,105,92,41,55,46,245,40,244, 
    102,143,54, 65,25,63,161, 1,216,80,73,209,76,132,187,208, 89,18,169,200,196, 
    135,130,116,188,159,86,164,100,109,198,173,186, 3,64,52,217,226,250,124,123, 
    5,202,38,147,118,126,255,82,85,212,207,206,59,227,47,16,58,17,182,189,28,42, 
    223,183,170,213,119,248,152, 2,44,154,163, 70,221,153,101,155,167, 43,172,9, 
    129,22,39,253, 19,98,108,110,79,113,224,232,178,185, 112,104,218,246,97,228, 
    251,34,242,193,238,210,144,12,191,179,162,241, 81,51,145,235,249,14,239,107, 
    49,192,214, 31,181,199,106,157,184, 84,204,176,115,121,50,45,127, 4,150,254, 
    138,236,205,93,222,114,67,29,24,72,243,141,128,195,78,66,215,61,156,180 
    ] 

next :: Permutation -> Word8 -> Word8 
next (Permutation !v) !idx' 
    = v `V.unsafeIndex` (fromIntegral $ idx' .&. 0xFF) 

E tutto questo è legato insieme con JuicyPixels:

main = do 
    [target] <- getArgs 
    let image = P.generateImage pixelRenderer 512 512 
    P.writePng target image 
    where 
    pixelRenderer, pixelRenderer' :: Int -> Int -> Word8 
    pixelRenderer !x !y 
     = floor $ ((perlin3 permutation ((fromIntegral x - 256)/32, 
      (fromIntegral y - 256)/32, 0 :: Double))+1)/2 * 128 

    -- This code is much more readable, but also much slower. 
    pixelRenderer' x y 
     = (\w -> floor $ ((w+1)/2 * 128)) -- w should be in [-1,+1] 
     . perlin3 permutation 
     . (\(x,y,z) -> ((x-256)/32, (y-256)/32, (z-256)/32)) 
     $ (fromIntegral x, fromIntegral y, 0 :: Double) 

mio problema è che perlin3 sembra molto lento per me. Se lo registro, lo pixelRenderer sta passando molto tempo, ma per ora lo ignorerò. Non so come ottimizzare perlin3. Ho provato a suggerire GHC con modelli di scoppio, il che riduce a metà il tempo di esecuzione dello , quindi è bello. Specializzarsi in modo esplicito e inlining aiuta appena con ghc -O. perlin3 dovrebbe essere così lento?


UPDATE: una versione precedente di questa domanda menzionato un bug nel mio codice. Questo problema è stato risolto; risulta che la mia vecchia versione di actuallyProperFraction era piena di bug. Ha implicitamente arrotondato la parte integrale di un numero in virgola mobile a Word8 e quindi sottratto dal numero in virgola mobile per ottenere la parte frazionaria. Poiché Word8 può assumere solo valori compresi tra 0 e 255 inclusi, questo non funzionerà correttamente per i numeri esterni all'intervallo, compresi i numeri negativi.

+0

Come lo si definisce? Il profiling con '-auto-all' disabilita alcune ottimizzazioni per il profilo in modo più accurato. Ottengo un fattore di rallentamento di 2.5 usando '-auto-all', relativo a' -auto'. – Heatsink

+0

Avevo 'ghc -O -o/tmp/IPerlin -prof -rtsopts -auto-all -caf-all -fforce-recomp IPerlin.lhs', quindi chiamandolo'/tmp/IPerlin + RTS -p -RTS/tmp/output.png'. '-auto' è davvero molto più veloce, ma ora il rapporto di profilazione contiene a malapena qualsiasi informazione (nessuna menzione di' perlin3'). Inoltre, so a malapena cosa dovrei cercare: P –

+0

Penso che 'grad' possa essere migliorato usando un tipo diverso per' vks'. L'istanza di 'Unbox' per le tuple le archivia effettivamente come una tupla di array. Se si crea un tipo triplo e un'istanza di unbox che memorizza i valori consecutivamente, dovrebbe essere un miglioramento. Rendere la tua tripla stretta semplificherebbe anche parte dell'altro codice. –

risposta

4

Questo codice sembra essere principalmente legato alla computazione. Può essere migliorato un po ', ma non di molto a meno che non ci sia un modo per usare meno ricerche di array e meno aritmetica.

Esistono due strumenti utili per misurare le prestazioni: profiling e dump di codice. Ho aggiunto un'annotazione SCC a perlin3 in modo che venisse visualizzata nel profilo. Quindi ho compilato con gcc -O2 -fforce-recomp -ddump-simpl -prof -auto. Il flag -ddump-simpl stampa il codice semplificato.

Profiling: Sul mio computer, ci vogliono 0,60 secondi per eseguire il programma, e circa il 20% del tempo di esecuzione (0,12 secondi) viene speso in perlin3 secondo il profilo. Si noti che la precisione delle informazioni del mio profilo è di circa +/- 3%.

Uscita semplificata: Il programma di semplificazione produce codice abbastanza pulito. perlin3 viene incorporato in pixelRenderer, quindi questa è la parte dell'output che si desidera esaminare.La maggior parte del codice è costituita da letture di array non archiviate e aritmetica non archiviata. Per migliorare le prestazioni, vogliamo eliminare parte di questa aritmetica.

Un semplice cambiamento consiste nell'eliminare i controlli di runtime su SomeFraction (che non appare nella domanda, ma fa parte del codice che è stato caricato). Ciò riduce il tempo di esecuzione del programma a 0,56 secondi.

-- someFraction t | 0 <= t, t < 1 = SomeFraction t 
someFraction t = SomeFraction t 

Avanti, ci sono diverse ricerche di matrice che appaiono nella semplificatore simili:

    case GHC.Prim.indexWord8Array# 
         ipv3_s23a 
         (GHC.Prim.+# 
          ipv1_s21N 
          (GHC.Prim.word2Int# 
           (GHC.Prim.and# 
           (GHC.Prim.narrow8Word# 
            (GHC.Prim.plusWord# ipv5_s256 (__word 1))) 
           (__word 255)))) 

L'operazione primitiva narrow8Word# è per costringere da un Int ad un Word8. Possiamo eliminare questa coercizione utilizzando Int anziché Word8 nella definizione di next.

next :: Permutation -> Int -> Int 
next (Permutation !v) !idx' 
    = fromIntegral $ v `V.unsafeIndex` (fromIntegral idx' .&. 0xFF) 

Questo riduce il tempo di esecuzione del programma a 0,54 secondi. Considerando solo il tempo trascorso in perlin3, il tempo di esecuzione è diminuito (all'incirca) da 0,12 a 0,06 secondi. Sebbene sia difficile misurare dove sta andando il resto del tempo, è molto probabile che si diffonda tra gli aritmetici e gli accessi di array rimanenti.

+0

Quindi penso che dovrei concentrarmi sull'ottimizzazione di 'grad' /' dot3' e della funzione di permutazione, se possibile. Grazie per il tempo di guardarlo :) Per notare, il controllo runtime 'someFraction' era solo per vedere se le mie supposizioni su certi valori erano (probabilmente) corrette, in modo che il controllo dovesse essere effettivamente rimosso nel codice di produzione. Lo guarderò oltre domani. –

2

Sul mio codice di riferimento macchina con ottimizzazioni del dissipatore di calore occorrono 0,19 secondi.

In primo luogo, si è spostato JuicyPixels-yarr e yarr-image-io con le mie bandiere preferiti, -Odph -rtsopts -threaded -fno-liberate-case -funbox-strict-fields -fexpose-all-unfoldings -funfolding-keeness-factor1000 -fsimpl-tick-factor=500 -fllvm -optlo-O3 (sono dati here):

import Data.Yarr as Y 
import Data.Yarr.IO.Image as Y 
... 

main = do 
    [target] <- getArgs 
    image <- dComputeS $ fromFunction (512, 512) (return . pixelRenderer) 
    Y.writeImage target (Grey image) 
    where 
    pixelRenderer, pixelRenderer' :: Dim2 -> Word8 
    pixelRenderer (y, x) 
     = floor $ ((perlin3 permutation ((fromIntegral x - 256)/32, 
      (fromIntegral y - 256)/32, 0 :: Double))+1)/2 * 128 

    -- This code is much more readable, but also much slower. 
    pixelRenderer' (y, x) 
     = (\w -> floor $ ((w+1)/2 * 128)) -- w should be in [-1,+1] 
     . perlin3 permutation 
     . (\(x,y,z) -> ((x-256)/32, (y-256)/32, (z-256)/32)) 
     $ (fromIntegral x, fromIntegral y, 0 :: Double) 

Questo rende il programma 30% più veloce, 0,13 secondi.

In secondo luogo mi ha sostituito usi di serie floor con

doubleToByte :: Double -> Word8 
doubleToByte f = fromIntegral (truncate f :: Int) 

E 'noto problema (google "performance piano Haskell"). Il tempo di esecuzione è ridotto a 52 ms (0,052 secondi), in quasi 3 volte.

Infine, solo per divertimento ho provato a calcolare il rumore in parallelo (dComputeP anziché dComputeS e +RTS -N4 in esecuzione da riga di comando). Il programma ha impiegato 36 ms, inclusa una costante I/O di circa 10 ms.

+0

Sebbene ciò non ottimizzi la funzione di rumore Perlin, riduce notevolmente il tempo di esecuzione totale. Sostituire 'floor' da solo è già un enorme incremento di prestazioni (da 5.42 s a 2.40 s, per un'immagine 4 Mipx). Non sono sicuro di voler passare a 'yarr-image-io' (non so quanto sia più difficile il packaging per Windows se inizio a utilizzare DevIL), ma certamente cercherò di approfondire. Grazie per il suggerimento e per mostrare la tua biblioteca! :) –

+0

@Rhymoid considera anche la sostituzione della lastra x-y-z con il flusso di controllo dalla libreria 'fixed-vector'. Es., 'Punto3 = somma. zipWith (*) '([sum] (http://hackage.haskell.org/packages/archive/fixed-vector/0.1.2.1/doc/html/Data-Vector-Fixed.html#v:sum), [ zipWith] (http://hackage.haskell.org/packages/archive/fixed-vector/0.1.2.1/doc/html/Data-Vector-Fixed.html#v:zipWith)) – leventov