2013-07-18 13 views
5

Tôi đang cố gắng tìm ra một số vấn đề về hiệu suất mà tôi đã gặp phải với Haskell. Là một phần trong đó, tôi đã viết một chương trình so sánh nhỏ để so sánh C và Haskell. Cụ thể, tôi đã dịch chương trình C sang Haskell với ít thay đổi nhất có thể. Phần đo tốc độ của chương trình Haskell sau đó được viết theo một phong cách rất bắt buộc.Tại sao Haskell thực hiện quá kém khi thực thi các mã giống như C? (trong trường hợp này ít nhất)

Chương trình tạo hai danh sách các số ngẫu nhiên trong một số phạm vi, sau đó tính tích phân của biểu đồ được hình thành bằng cách kết nối các điểm đó với một danh sách là x-giá trị và một danh sách là giá trị y. Về cơ bản, nó là trapezoidal rule.

Dưới đây là hai mã:

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 

Việc tích hợp C chạy trong khoảng 7 ms và sự hội nhập Haskell trong khoảng 60 ms trên hệ thống của tôi. Tất nhiên phiên bản Haskell sẽ chậm hơn, nhưng tôi tự hỏi tại sao nó lại chậm hơn nhiều. Rõ ràng có rất nhiều sự thiếu hiệu quả trong mã Haskell.

Tại sao mã Haskell chậm hơn rất nhiều? Làm thế nào có thể sửa chữa nó?

Cảm ơn mọi câu trả lời.

Trả lời

11

Ra khỏi tò mò, tôi chạy này với llvm:

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

và nó lấy nó xuống từ 60ms đến 24ms. Vẫn không lý tưởng.

Vì vậy, một trong những điều đầu tiên tôi sẽ làm khi tôi muốn biết lý do tại sao điểm chuẩn như thế này quá chậm, là đổ lõi chuẩn bị. Đó là, cốt lõi sau khi tối ưu hóa.

GHC Test.hs -O2 -ddump-prep -dsuppress-tất cả -XBangPatterns> Test.hscore

Sau khi xem qua các lõi, tôi cuối cùng đã tìm thấy $ wa, nơi vòng lặp được xác định . Nó chỉ ra nó làm cho đáng ngạc nhiên nhiều kiểm tra ràng buộc chỉ mục. Hãy xem, tôi thường sử dụng Data.Vector.Unboxed, có chức năng "unsafeRead" và "unsafeIndex", để loại bỏ kiểm tra giới hạn. Đây sẽ là hữu ích ở đây. Cá nhân, tôi nghĩ rằng gói vector cao hơn.

Nếu bạn nhìn vào $ wa, bạn sẽ nhận thấy nó unboxing các đối số vào lúc bắt đầu:

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

điều này có vẻ xấu, nhưng nó quay ra trong đệ quy gọi nó sử dụng một phiên bản đặc biệt, integrate_ $ s $ wa, với số nguyên unboxed vv Điều này là tốt.

Tóm lại, tôi nghĩ bạn nên cải thiện tốt bằng cách sử dụng vectơ với lập chỉ mục không an toàn.

Chỉnh sửa: đây là phiên bản được sửa đổi với Data.Vector. Nó chạy trong khoảng 7ms.Đối với mã số tốt, tôi nghĩ sự chậm chạp duy nhất so với C phải do phân tích bí danh chưa đầy đủ. https://gist.github.com/amosr/6026995

+2

Lệnh 'gói array' có 'unsafeRead' và' unsafeWrite' quá, không có cần phải chuyển sang 'vector' cho điều đó. –

+0

Ồ, được rồi. Tôi đã có một cái nhìn nhanh chóng nhưng không thể nhìn thấy chúng. Rõ ràng quá nhanh –

+0

@DanielFischer Tôi không thấy những phương thức đó trong giao diện MArray. Chúng có thực sự được thực thi khi các mảng có thể được lập chỉ mục bởi bất kỳ phần tử nào đang triển khai 'Ix' không? –

7

Trước tiên, tôi đã cố gắng mã của bạn để tái phát hiện của bạn (sử dụng GHC 7.6.3 -O2 -fllvm và gcc 4.7.2 và O3)

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

Vì vậy, chúng tôi đang hướng tới 10ms nếu mục tiêu là thực hiện ngang hàng (giảm 60% thời gian chạy). Nhìn vào mã của bạn, tôi thấy:

  • Array s được sử dụng, cổ xưa và thô lỗ. Tôi đã chuyển sang Vector.
  • Không có chuyển đổi công nhân/bao bọc trên iterative. Sự thay đổi chỉ là tạo một hàm phụ ở một mệnh đề where mà không yêu cầu xy làm tham số.
  • Float được sử dụng mặc dù Double thường hoạt động tốt hơn đáng ngạc nhiên (điều này có thể không quan trọng ở đây).

Kết quả cuối cùng là trên ngang hàng với những gì bạn được đăng trong C:

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

Một vài điểm nhỏ: công nhân/wrapper trên lặp đi lặp lại không quan trọng nhiều, vì tôi nghi ngờ SpecConstr (một tổng quát của w/w) sẽ làm điều đó anyway. Tôi cũng nghĩ rằng bạn cần một seq trong iterative để tính toán thời gian chính xác –

+0

Làm thế nào ngớ ngẩn của tôi, dạy cho tôi để quên '$!'. –

+0

Mặc dù bạn đúng - viết nó theo cách đảm bảo w/w sẽ xảy ra có lẽ tốt hơn là cầu nguyện cho SpecConstr và các tối ưu khác để làm điều đúng –

Các vấn đề liên quan