Làm thế nào mà numpy có thể nhanh hơn nhiều so với thói quen Fortran của tôi?


82

Tôi nhận được một mảng 512 ^ 3 biểu thị sự phân bố Nhiệt độ từ một mô phỏng (được viết bằng Fortran). Mảng được lưu trữ trong một tệp nhị phân có kích thước khoảng 1 / 2G. Tôi cần biết mức tối thiểu, tối đa và trung bình của mảng này và vì tôi sẽ sớm hiểu mã Fortran, tôi quyết định thử và nghĩ ra quy trình rất dễ dàng sau đây.

  integer gridsize,unit,j
  real mini,maxi
  double precision mean

  gridsize=512
  unit=40
  open(unit=unit,file='T.out',status='old',access='stream',&
       form='unformatted',action='read')
  read(unit=unit) tmp
  mini=tmp
  maxi=tmp
  mean=tmp
  do j=2,gridsize**3
      read(unit=unit) tmp
      if(tmp>maxi)then
          maxi=tmp
      elseif(tmp<mini)then
          mini=tmp
      end if
      mean=mean+tmp
  end do
  mean=mean/gridsize**3
  close(unit=unit)

Quá trình này mất khoảng 25 giây cho mỗi tệp trên máy tôi sử dụng. Điều đó khiến tôi cảm thấy khá lâu và vì vậy tôi đã tiếp tục và thực hiện những việc sau bằng Python:

    import numpy

    mmap=numpy.memmap('T.out',dtype='float32',mode='r',offset=4,\
                                  shape=(512,512,512),order='F')
    mini=numpy.amin(mmap)
    maxi=numpy.amax(mmap)
    mean=numpy.mean(mmap)

Bây giờ, tôi đã mong đợi điều này tất nhiên sẽ nhanh hơn, nhưng tôi đã thực sự bị thổi bay. Nó mất ít hơn một giây trong các điều kiện giống hệt nhau. Giá trị trung bình khác với giá trị mà tôi thường xuyên tìm thấy trong Fortran (mà tôi cũng đã chạy với phao 128 bit, vì vậy bằng cách nào đó tôi tin tưởng nó hơn) nhưng chỉ ở chữ số có nghĩa thứ 7 hoặc lâu hơn.

Làm thế nào mà numpy có thể nhanh như vậy? Ý tôi là bạn phải xem mọi mục nhập của một mảng để tìm những giá trị này, phải không? Tôi có đang làm điều gì đó rất ngu ngốc trong thói quen Fortran của mình để nó mất nhiều thời gian hơn không?

BIÊN TẬP:

Để trả lời các câu hỏi trong phần bình luận:

  • Có, tôi cũng đã chạy quy trình Fortran với phao 32 bit và 64 bit nhưng nó không ảnh hưởng đến hiệu suất.
  • Tôi đã sử dụng iso_fortran_envcung cấp phao 128 bit.
  • Mặc dù vậy, việc sử dụng phao 32-bit có nghĩa là hơi sai, vì vậy độ chính xác thực sự là một vấn đề.
  • Tôi đã chạy cả hai quy trình trên các tệp khác nhau theo thứ tự khác nhau, do đó, bộ nhớ đệm lẽ ra phải công bằng khi so sánh?
  • Tôi thực sự đã thử mở MP, nhưng để đọc từ tệp ở các vị trí khác nhau cùng một lúc. Sau khi đọc các bình luận và câu trả lời của bạn, điều này nghe có vẻ thực sự ngu ngốc và nó cũng làm cho quy trình mất nhiều thời gian hơn. Tôi có thể thử các phép toán mảng nhưng có lẽ điều đó thậm chí sẽ không cần thiết.
  • Các tệp thực sự có kích thước 1 / 2G, đó là lỗi đánh máy, Cảm ơn.
  • Tôi sẽ thử triển khai mảng ngay bây giờ.

CHỈNH SỬA 2:

Tôi đã triển khai những gì @Alexander Vogt và @casey đề xuất trong câu trả lời của họ và nó nhanh như numpyvậy nhưng bây giờ tôi gặp vấn đề về độ chính xác như @Luaan đã chỉ ra mà tôi có thể gặp phải. Sử dụng mảng float 32 bit, giá trị trung bình được tính bằng giảm sum20%. Đang làm

...
real,allocatable :: tmp (:,:,:)
double precision,allocatable :: tmp2(:,:,:)
...
tmp2=tmp
mean=sum(tmp2)/size(tmp)
...

Giải quyết vấn đề nhưng làm tăng thời gian tính toán (không quá nhiều, nhưng đáng chú ý). Có cách nào tốt hơn để giải quyết vấn đề này không? Tôi không thể tìm thấy cách đọc đĩa đơn từ tệp trực tiếp đến đĩa đôi. Và làm thế nào để numpytránh điều này?

Cảm ơn tất cả sự giúp đỡ cho đến nay.


10
Bạn đã thử quy trình Fortran không có phao 128-bit chưa? Tôi không biết bất kỳ phần cứng nào thực sự hỗ trợ chúng, vì vậy chúng phải được thực hiện trong phần mềm.
user2357112 hỗ trợ Monica

4
Điều gì sẽ xảy ra nếu bạn thử phiên bản Fortran bằng cách sử dụng một mảng (và đặc biệt là sử dụng một lần đọc thay vì một tỷ)?
francescalus

9
Bạn có cân nhắc sử dụng cả toán tử mảng trong Fortran không? Sau đó, bạn có thể thử minval(), maxval()sum()? Hơn nữa, bạn đang trộn IO với các hoạt động trong Fortran, nhưng không phải bằng Python - đó không phải là một sự so sánh công bằng ;-)
Alexander Vogt

4
Khi đo điểm chuẩn cho một thứ gì đó liên quan đến một tệp lớn, hãy đảm bảo rằng nó được lưu vào bộ nhớ đệm giống nhau cho tất cả các lần chạy.
Tom Zych

1
Cũng lưu ý rằng độ chính xác là một vấn đề khá lớn trong Fortran và nó phải trả giá. Ngay cả sau khi bạn khắc phục tất cả các vấn đề rõ ràng đó với mã Fortran của mình, rất có thể cần phải có thêm độ chính xác và sẽ gây ra giảm tốc độ đáng kể.
Luaan

Câu trả lời:


110

Việc triển khai Fortran của bạn gặp phải hai thiếu sót lớn:

  • Bạn kết hợp IO và tính toán (và đọc từ mục nhập tệp theo mục nhập).
  • Bạn không sử dụng các phép toán vectơ / ma trận.

Việc triển khai này thực hiện cùng một thao tác như của bạn và nhanh hơn bởi hệ số 20 trên máy của tôi:

program test
  integer gridsize,unit
  real mini,maxi,mean
  real, allocatable :: tmp (:,:,:)

  gridsize=512
  unit=40

  allocate( tmp(gridsize, gridsize, gridsize))

  open(unit=unit,file='T.out',status='old',access='stream',&
       form='unformatted',action='read')
  read(unit=unit) tmp

  close(unit=unit)

  mini = minval(tmp)
  maxi = maxval(tmp)
  mean = sum(tmp)/gridsize**3
  print *, mini, maxi, mean

end program

Ý tưởng là đọc toàn bộ tệp thành một mảng tmptrong một lần. Sau đó, tôi có thể sử dụng các chức năng MAXVAL, MINVALSUMtrên mảng trực tiếp.


Đối với vấn đề về độ chính xác: Chỉ cần sử dụng các giá trị độ chính xác kép và thực hiện chuyển đổi nhanh chóng như

mean = sum(real(tmp, kind=kind(1.d0)))/real(gridsize**3, kind=kind(1.d0))

chỉ làm tăng thời gian tính toán một chút. Tôi đã thử thực hiện theo phần tử hoạt động và theo từng phần, nhưng điều đó chỉ làm tăng thời gian cần thiết ở mức tối ưu hóa mặc định.

Tại -O3, phép cộng phần tử hoạt động tốt hơn ~ 3% so với thao tác mảng. Sự khác biệt giữa các hoạt động chính xác kép và đơn là ít hơn 2% trên máy của tôi - trung bình (cá nhân chạy sai lệch nhiều hơn).


Đây là một triển khai rất nhanh bằng cách sử dụng LAPACK:

program test
  integer gridsize,unit, i, j
  real mini,maxi
  integer  :: t1, t2, rate
  real, allocatable :: tmp (:,:,:)
  real, allocatable :: work(:)
!  double precision :: mean
  real :: mean
  real :: slange

  call system_clock(count_rate=rate)
  call system_clock(t1)
  gridsize=512
  unit=40

  allocate( tmp(gridsize, gridsize, gridsize), work(gridsize))

  open(unit=unit,file='T.out',status='old',access='stream',&
       form='unformatted',action='read')
  read(unit=unit) tmp

  close(unit=unit)

  mini = minval(tmp)
  maxi = maxval(tmp)

!  mean = sum(tmp)/gridsize**3
!  mean = sum(real(tmp, kind=kind(1.d0)))/real(gridsize**3, kind=kind(1.d0))
  mean = 0.d0
  do j=1,gridsize
    do i=1,gridsize
      mean = mean + slange('1', gridsize, 1, tmp(:,i,j),gridsize, work)
    enddo !i
  enddo !j
  mean = mean / gridsize**3

  print *, mini, maxi, mean
  call system_clock(t2)
  print *,real(t2-t1)/real(rate)

end program

Điều này sử dụng ma trận chính xác đơn 1-tiêu chuẩn SLANGEtrên các cột ma trận. Thời gian chạy thậm chí còn nhanh hơn so với phương pháp sử dụng các hàm mảng chính xác đơn lẻ - và không cho thấy vấn đề về độ chính xác.


4
Tại sao trộn đầu vào với tính toán lại làm chậm quá nhiều? Cả hai đều phải đọc toàn bộ tệp, đó sẽ là nút thắt cổ chai. Và nếu hệ điều hành có readahead, mã Fortran sẽ không phải đợi nhiều cho I / O.
Barmar

3
@Barmar Bạn vẫn sẽ có chi phí gọi hàm và logic để kiểm tra xem dữ liệu có trong bộ nhớ cache hay không.
Overv

55

Numpy nhanh hơn vì bạn đã viết mã hiệu quả hơn nhiều trong python (và phần lớn chương trình phụ trợ numpy được viết bằng Fortran và C được tối ưu hóa) và mã cực kỳ kém hiệu quả trong Fortran.

Nhìn vào mã python của bạn. Bạn tải toàn bộ mảng cùng một lúc và sau đó gọi các hàm có thể hoạt động trên một mảng.

Nhìn vào mã fortran của bạn. Bạn đọc từng giá trị một và thực hiện một số logic phân nhánh với nó.

Phần lớn sự khác biệt của bạn là IO bị phân mảnh mà bạn đã viết trong Fortran.

Bạn có thể viết Fortran giống như cách bạn viết python và bạn sẽ thấy nó chạy nhanh hơn nhiều theo cách đó.

program test
  implicit none
  integer :: gridsize, unit
  real :: mini, maxi, mean
  real, allocatable :: array(:,:,:)

  gridsize=512
  allocate(array(gridsize,gridsize,gridsize))
  unit=40
  open(unit=unit, file='T.out', status='old', access='stream',&
       form='unformatted', action='read')
  read(unit) array    
  maxi = maxval(array)
  mini = minval(array)
  mean = sum(array)/size(array)
  close(unit)
end program test

Có giá trị trung bình tính theo cách này có được độ chính xác tương tự như numpy's .meancuộc gọi? Tôi có một số nghi ngờ về điều đó.
Bakuriu

1
@Bakuriu Không, không. Xem câu trả lời của Alexander Vogt và các chỉnh sửa của tôi về câu hỏi.
user35915,
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.