2013-08-22 3 views
4

하스켈과 스칼라에는 매우 간단한 코드가 있습니다. 이 코드는 매우 엄격한 루프에서 실행되도록 설계되었으므로 성능이 중요합니다. 문제는 Haskell이 Scala보다 약 10 배 더 느리다는 것입니다. 여기는 하스켈 코드입니다.하스켈 벡터 성능이 스칼라와 비교

{-# LANGUAGE BangPatterns #-} 
import qualified Data.Vector.Unboxed as VU 

newtype AffineTransform = AffineTransform {get :: (VU.Vector Double)} deriving (Show) 

{-# INLINE runAffineTransform #-} 
runAffineTransform :: AffineTransform -> (Double, Double) -> (Double, Double) 
runAffineTransform affTr (!x, !y) = (get affTr `VU.unsafeIndex` 0 * x + get affTr `VU.unsafeIndex` 1 * y + get affTr `VU.unsafeIndex` 2, 
             get affTr `VU.unsafeIndex` 3 * x + get affTr `VU.unsafeIndex` 4 * y + get affTr `VU.unsafeIndex` 5) 

testAffineTransformSpeed :: AffineTransform -> Int -> (Double, Double) 
testAffineTransformSpeed affTr count = go count (0.5, 0.5) 
    where go :: Int -> (Double, Double) -> (Double, Double) 
     go 0 res = res 
     go !n !res = go (n-1) (runAffineTransform affTr res) 

이 코드를 개선하기 위해 더 많은 작업을 수행 할 수 있습니까?

+0

어떻게 컴파일하고 컴파일러를 사용 했습니까? –

+0

ghc 7.6.3으로 컴파일되었습니다.옵션은 "-O2 -Wall -funbox-strict-fields -threaded -rtsopts"입니다. 나는 funbox-strict-fields가 충분하다고 예상했지만, 사실이 아니었다. 글쎄, 나는 완전 초보자 야. 그래서 내 기대는 조금 벗어날지도 모른다. – Aurimas

+1

어파 인 변형을 표현하기 위해 왜 '벡터'가 필요한가요? ADT를 만들고 좌표 배열을 처리하는 것이 더 합리적 인 것처럼 보입니다. – leventov

답변

8

주요 문제는

runAffineTransform affTr (!x, !y) = (get affTr `VU.unsafeIndex` 0 * x 
            + get affTr `VU.unsafeIndex` 1 * y 
            + get affTr `VU.unsafeIndex` 2, 
             get affTr `VU.unsafeIndex` 3 * x 
            + get affTr `VU.unsafeIndex` 4 * y 
            + get affTr `VU.unsafeIndex` 5) 

썽크 쌍을 생성한다는 것이다. 구성 요소는 runAffineTransform이 호출 될 때 평가되지 않고 일부 소비자가 평가를 요청할 때까지 뚝 떨어집니다.

testAffineTransformSpeed affTr count = go count (0.5, 0.5) 
    where go :: Int -> (Double, Double) -> (Double, Double) 
     go 0 res = res 
     go !n !res = go (n-1) (runAffineTransform affTr res) 

는 소비자, res에 강타는 가장 바깥 쪽 생성자, (,)에 평가하지, 당신은 말만을 평가

runAffineTransform affTr (runAffineTrasform affTr (runAffineTransform affTr (...))) 

의 결과를 얻을 때 마지막으로 정상적인 형태가 요구된다.

당신은 결과의 구성 요소를 강제하는 경우

그것을 인라인, 바로

runAffineTransform affTr (!x, !y) = case 
    ( get affTr `U.unsafeIndex` 0 * x 
    + get affTr `U.unsafeIndex` 1 * y 
    + get affTr `U.unsafeIndex` 2 
    , get affTr `U.unsafeIndex` 3 * x 
    + get affTr `U.unsafeIndex` 4 * y 
    + get affTr `U.unsafeIndex` 5 
) of (!a,!b) -> (a,b) 

을 평가하게 될, 언 박싱 Double# s의 사용자 정의 엄격한 쌍을 사용하여 jtobin의 버전의 주요 차이점은 대한 그 testAffineTransformSpeed의 루프는 boxed Double을 인수로 사용하여 하나의 초기 반복을 얻습니다. 결과적으로 결과의 구성 요소에 상자가 생기고 일정한 오버 헤드가 추가됩니다 (내 상자의 루프 당 약 5 나노초). 루프의 주요 부분은 두 경우 모두 Int# 및 두 개의 Double# 인수를 취하며 루프 본문은 n = 0에 도달하면 복싱을 제외하고 동일합니다.

물론 상자가없는 엄격한 쌍 유형을 사용하여 구성 요소를 즉시 평가하도록하는 것이 좋습니다.

+0

언제'! (! x,! y)'가 유용할까요? – is7s

+0

'let'또는 'where'바인딩에서. 하지만 여기에서는'! res @ (! x,! y)'에서 너무 적은 문자 하나를 지우는 것만 남을 것입니다. 주의 해 주셔서 감사합니다. –

9
나는 다음과 같은 엄격한/박스 없음 쌍 유형 정의

:

import System.Random.MWC -- for later 
import Control.DeepSeq 

data SP = SP { 
    one :: {-# UNPACK #-} !Double 
    , two :: {-# UNPACK #-} !Double 
    } deriving Show 

instance NFData SP where 
    rnf p = rnf (one p) `seq` rnf (two p) `seq`() 

을하고 runAffineTransform 기능에 대체 :

runAffineTransform2 :: AffineTransform -> SP -> SP 
runAffineTransform2 affTr !(SP x y) = 
    SP ( get affTr `U.unsafeIndex` 0 * x 
     + get affTr `U.unsafeIndex` 1 * y 
     + get affTr `U.unsafeIndex` 2 ) 

    ( get affTr `U.unsafeIndex` 3 * x 
     + get affTr `U.unsafeIndex` 4 * y 
     + get affTr `U.unsafeIndex` 5 ) 
{-# INLINE runAffineTransform2 #-} 

다음이 벤치 마크 스위트 실행 :

main :: IO() 
main = do 
    g <- create 
    zs <- fmap (AffineTransform . U.fromList) 
      (replicateM 100000 (uniformR (0 :: Double, 1) g)) 

    let myConfig = defaultConfig { cfgPerformGC = ljust True } 

    defaultMainWith myConfig (return()) [ 
     bench "yours" $ nf (testAffineTransformSpeed zs) 10 
    , bench "mine" $ nf (testAffineTransformSpeed2 zs) 10 
    ] 

를 컴파일 -O2으로 실행하고 몇 가지 (~ 4x) 속도 향상을 관찰했습니다 :

benchmarking yours 
mean: 257.4559 ns, lb 256.2492 ns, ub 258.9761 ns, ci 0.950 
std dev: 6.889905 ns, lb 5.688330 ns, ub 8.839753 ns, ci 0.950 
found 5 outliers among 100 samples (5.0%) 
    3 (3.0%) high mild 
    2 (2.0%) high severe 
variance introduced by outliers: 20.944% 
variance is moderately inflated by outliers 

benchmarking mine 
mean: 69.56408 ns, lb 69.29910 ns, ub 69.86838 ns, ci 0.950 
std dev: 1.448874 ns, lb 1.261444 ns, ub 1.718074 ns, ci 0.950 
found 4 outliers among 100 samples (4.0%) 
    4 (4.0%) high mild 
variance introduced by outliers: 14.190% 
variance is moderately inflated by outliers 

전체 코드는입니다. 나는 또한 기준의 출력 보고서 here을 게시

편집 할 수 있습니다.

+1

내 컴퓨터에서 스칼라 (Java)보다 성능이 약간 우수합니다. 그리고 배울 교훈은 무엇입니까? 엄격한 주석으로는 충분하지 않습니다 (자동으로 unbox되지 않습니까?)? 때로는 unboxed (압축을 푼) 데이터 구조를 수동으로 만들어야합니까? – Aurimas

+1

@ user2705843 슬라이드 (4)와 (9)는 다음과 관련이 있습니다. http://johantibell.com/files/haskell-performance-patterns.html – jtobin

+2

'SP '의'NFData' 인스턴스가 완전히 낭비되고 있습니다. 사실 꽤 많이 있습니다. 이것은 암시 적으로'SP'에서 언 패킹 된 값을 가리 키도록 강제로'Double' 생성자를 생성하고 강제적으로 생성 한 다음에 버립니다. 'rnf a = seq a()'는 훨씬 더 효율적이고 정확합니다. – Carl