2013-07-18 11 views
5

Próbuję wymyślić pewne problemy z wydajnością, które miałem z Haskellem. W ramach tego napisałem mały program porównawczy, aby porównać C i Haskell. W szczególności przetłumaczyłem program C do Haskella z jak najmniejszymi zmianami, jakie mogłem. Zmierzona w prędkości część programu Haskell jest napisana w bardzo imperatywnym stylu.Dlaczego Haskell działa tak źle podczas wykonywania kodów typu C? (w tym przypadku co najmniej)

Program tworzy dwie listy liczb losowych w pewnym zakresie, a następnie oblicza całkę wykresu utworzoną przez proste połączenie tych punktów, z jedną listą będącą wartością x i jedną listą będącą wartością y. Zasadniczo jest to trapezoidal rule.

Tutaj oba kody:

main.c

#include <stdio.h> 
#include <stdlib.h> 
#include <time.h> 

#define N 5000000 
#define maxY 1e5f/N 
#define maxXgap 1 

int main(){ 
    int i; 
    float *y, *x; 
    float xaccum, area; 
    clock_t begin, end; 
    double time_spent; 

    y = (float*)malloc(sizeof(float)*N); 
    x = (float*)malloc(sizeof(float)*N); 

    srand(50546345); // change seed for different numbers 

    //populate y and x fields with random points 
    for(i = 0; i < N; i++){ 
     y[i] = ((float)rand())/((float)RAND_MAX)*maxY; 
    } 
    xaccum = 0; 
    for(i = 0; i < N; i++){ 
     x[i] = xaccum; 
     xaccum += ((float)rand())/((float)RAND_MAX)*maxXgap; 
    } 
    begin = clock(); 
    //perform a trapezoidal integration using the x y coordinates 
    area = 0; 
    for(i = 0; i < N-1; i++){ 
     area += (y[i+1]+y[i])/2*(x[i+1]-x[i]); 
    } 
    end = clock(); 
    time_spent = (double)(end - begin)/CLOCKS_PER_SEC * 1000; 
    printf("%i points\n%f area\n%f ms\n", N, area, time_spent); 
} 

Main.hs

{-# LANGUAGE BangPatterns #-} 
module Main where 

import Data.Array.Unboxed 
import Data.Array.IO 
import Data.List 
import System.Random 
import System.CPUTime 
import Text.Printf 
import Control.Exception 

main :: IO() 
main = do 
      (x,y) <- initArrays 
      area <- time $ integrate x y 
      print area 

n :: Int 
n = 5000000 

maxY :: Float 
maxY = 100000.0/(fromIntegral n) 

maxXgap :: Float 
maxXgap = 1 

--initialize arrays with random floats 
--this part is not measured in the running time (very slow) 
initArrays :: IO (IOUArray Int Float, IOUArray Int Float) 
initArrays = do 
       y <- newListArray (0,n-1) (randomList maxY n (mkStdGen 23432)) 
       x <- newListArray (0,n-1) (scanl1 (+) $ randomList maxXgap n (mkStdGen 5462)) 
       return (x,y) 

randomList :: Float -> Int -> StdGen -> [Float] 
randomList max n gen = map (abs . ((*) max)) (take n . unfoldr (Just . random) $ gen) 

integrate :: IOUArray Int Float -> IOUArray Int Float -> IO Float 
integrate x y = iterative x y 0 0 

iterative :: IOUArray Int Float -> IOUArray Int Float -> Int -> Float -> IO Float 
iterative x y !i !accum = do if i == n-1 
           then return accum 
           else do x1 <- readArray x i 
             x2 <- readArray x (i+1) 
             y1 <- readArray y i 
             y2 <- readArray y (i+1) 
             iterative x y (i+1) (accum + (y2+y1)/2*(x2-x1)) 

time :: IO t -> IO t 
time a = do 
      start <- getCPUTime 
      v <- a 
      end <- getCPUTime 
      let diff = (fromIntegral (end-start))/(10^9) 
      printf "Computation time %0.5f ms\n" (diff :: Double) 
      return v 

Integracja C trwa około 7 ms do integracji Haskell około 60 ms w moim systemie. Oczywiście wersja Haskell będzie wolniejsza, ale zastanawiam się, dlaczego jest o wiele wolniej. Oczywiście w kodzie Haskella jest dużo nieefektywności.

Dlaczego kod Haskella jest znacznie wolniejszy? Jak można to naprawić?

Dzięki za wszelkie odpowiedzi.

Odpowiedz

11

Z ciekawości, wpadłem to z LLVM:

ghc Test.hs -O2 -XBangPatterns -fllvm -optlo-O3

i zajęło go od 60ms do 24ms. Nadal nie jest idealny.

Tak więc, jedną z pierwszych rzeczy, które zrobię, gdy chcę się dowiedzieć, dlaczego taki benchmark jest taki powolny, jest zrzucenie przygotowanego rdzenia. To jest rdzeń po optymalizacji.

ghc Test.hs -O2 -ddump-prep -dsuppress wszystko -XBangPatterns> Test.hscore

Po patrząc przez rdzeń, w końcu znalazł $ WA, gdzie pętla iteracyjna jest zdefiniowana . Okazuje się, że robi zaskakująco wiele sprawdzonych indeksów. Zobacz, zwykle używam Data.Vector.Unboxed, który ma funkcje "unsafeRead" i "unsafeIndex", aby usunąć kontrole graniczne. Przydadzą się tutaj. Osobiście uważam, że pakiet wektorowy jest lepszy.

Jeśli spojrzeć na $ wa, zauważysz to unboxing argumenty na początku:

case w_s3o9 of _ { STUArray l_s3of u_s3oi ds1_s3ol ds2_s3oH -> 
case l_s3of of wild2_s3os { I# m_s3oo -> 
case u_s3oi of wild3_s3ot { I# n1_s3ov -> 
case ds1_s3ol of wild4_s3oC { I# y1_s3oE -> 

to wygląda źle, ale okazuje się w rekurencyjne nazywają to przy użyciu specjalistycznego wersji integrate_ $ s $ wa, z niepodzielonymi liczbami całkowitymi itd. To jest dobre.

Podsumowując, uważam, że powinieneś uzyskać dobrą poprawę dzięki użyciu wektora z niebezpiecznym indeksowaniem.

Edytuj: tutaj jest zmodyfikowana wersja z Data.Vector. Działa w około 7ms.Dla dobry kod wektorowy, myślę, że jedyne spowolnienie w porównaniu do C powinno być spowodowane niepełną analizą aliasów. https://gist.github.com/amosr/6026995

+2

Opcja 'pakiet array' ma' 'unsafeWrite' unsafeRead' i też nie ma potrzeby, aby przejść do' vector' za to. –

+0

Oh, okay. Spojrzałem szybko, ale nie mogłem ich zobaczyć. Oczywiście zbyt szybko –

+0

@ Daniel Fischer Nie widzę tych metod w interfejsie MArray. Czy naprawdę mogą być zaimplementowane, gdy tablice mogą być indeksowane przez dowolny element implementujący 'Ix'? –

7

Najpierw próbowałem kod odtworzyć swoje spostrzeżenia (używając GHC 7.6.3 -O2 -fllvm i gcc 4.7.2 i -O3)

$ ./theHaskellVersion-rev1 
Computation time 24.00000 ms 
25008.195 
[[email protected] Test]$ ./theCVersion 
5000000 points 
25013.105469 area 
10.000000 ms 

Dlatego dążymy do 10ms, jeżeli celem jest osiągnięcie wartości nominalnej (zmniejszenie czasu działania o 60%). Patrząc na twój kod widzę:

  • s są używane, które są starożytne i kruchy. Przełączyłem się na Vector.
  • Brak transformacji pracownik/opakowanie na iterative. Zmiana ma na celu tylko utworzenie funkcji pomocniczej w klauzuli where, która nie wymaga x i y jako parametrów.
  • Float jest stosowany, mimo że Double często działa zaskakująco lepiej (to chyba nie ma znaczenia tutaj).

Efektem końcowym jest na równi z tym, co pisał w C:

$ ghc -O2 so.hs -hide-package random && ./so 
Computation time 11.00000 ms 
24999.048783785303 
+1

Kilka drobnych punktów: pracownik/opakowanie w iteracji nie ma większego znaczenia, ponieważ podejrzewam, że SpecConstr (uogólnienie w/w) zrobi to mimo wszystko. Też myślę, że potrzebujesz seq w iteracji, aby poprawnie obliczyć czasy. –

+0

Jak głupio ze mnie, uczy mnie zapomnieć '$!'. –

+0

Chociaż masz rację - pisanie w taki sposób, aby zapewnić w/w, że wystąpi, jest prawdopodobnie lepsze niż modlenie się o SpecConstr i inne optymalizacje, aby postąpili słusznie. –

Powiązane problemy