2015-11-17 15 views
41

Tôi đang viết một số mã hiệu năng quan trọng trong Swift. Sau khi thực hiện tất cả các tối ưu hóa mà tôi có thể nghĩ đến, và lược tả ứng dụng trong Instruments, tôi đã nhận ra rằng phần lớn các chu kỳ CPU được dùng để thực hiện các hoạt động map()reduce() trên mảng Floats. Vì vậy, chỉ để xem điều gì sẽ xảy ra, tôi đã thay thế tất cả các trường hợp mapreduce bằng các vòng lặp cũ for. Và sự ngạc nhiên của tôi ... các vòng for là nhiều, nhanh hơn nhiều!Hiệu suất Swift: map() và reduce() vs cho vòng lặp

Một chút bối rối bởi điều này, tôi quyết định thực hiện một số tiêu chuẩn thô. Trong một thử nghiệm, tôi đã map trở lại một mảng của Floats sau khi thực hiện một số số học đơn giản như vậy:

// Populate array with 1,000,000,000 random numbers 
var array = [Float](count: 1_000_000_000, repeatedValue: 0) 
for i in 0..<array.count { 
    array[i] = Float(random()) 
} 
let start = NSDate() 
// Construct a new array, with each element from the original multiplied by 5 
let output = array.map({ (element) -> Float in 
    return element * 5 
}) 
// Log the elapsed time 
let elapsed = NSDate().timeIntervalSinceDate(start) 
print(elapsed) 

Và tương đương thực hiện for loop:

var output = [Float]() 
for element in array { 
    output.append(element * 5) 
} 

thời gian thực hiện trung bình cho map: 20,1 giây. Thời gian thực hiện trung bình cho vòng lặp for: 11,2 giây. Kết quả tương tự bằng cách sử dụng số nguyên thay vì nổi.

Tôi đã tạo một điểm chuẩn tương tự để kiểm tra hiệu suất của Swift reduce. Lần này, các vòng reducefor đạt được hiệu suất gần như giống nhau khi tổng hợp các phần tử của một mảng lớn. Nhưng khi tôi vòng lặp kiểm tra 100.000 lần như thế này:

// Populate array with 1,000,000 random numbers 
var array = [Float](count: 1_000_000, repeatedValue: 0) 
for i in 0..<array.count { 
    array[i] = Float(random()) 
} 
let start = NSDate() 
// Perform operation 100,000 times 
for _ in 0..<100_000 { 
    let sum = array.reduce(0, combine: {$0 + $1}) 
} 
// Log the elapsed time 
let elapsed = NSDate().timeIntervalSinceDate(start) 
print(elapsed) 

vs:

for _ in 0..<100_000 { 
    var sum: Float = 0 
    for element in array { 
     sum += element 
    } 
} 

Phương pháp reduce mất 29 giây trong khi for loop mất (rõ ràng) 0,000003 giây.

Đương nhiên tôi đã sẵn sàng bỏ qua thử nghiệm cuối cùng là kết quả tối ưu hóa trình biên dịch, nhưng tôi nghĩ nó có thể cung cấp một số thông tin chi tiết về cách trình biên dịch tối ưu hóa khác nhau cho các vòng lặp so với các phương thức mảng tích hợp của Swift. Lưu ý rằng tất cả các thử nghiệm được thực hiện với tối ưu hóa -Os trên một i7 MacBook Pro 2,5 GHz. Kết quả khác nhau tùy thuộc vào kích thước mảng và số lần lặp lại, nhưng vòng lặp for luôn hoạt động tốt hơn các phương pháp khác ít nhất 1.5x, đôi khi lên tới 10x.

Tôi hơi bối rối về hiệu suất của Swift tại đây. Các phương thức Array tích hợp có nhanh hơn phương pháp ngây thơ để thực hiện các hoạt động như vậy không? Có lẽ ai đó có kiến ​​thức cấp thấp hơn tôi có thể làm sáng tỏ tình hình.

+1

Rất có thể, trình biên dịch nhận ra rằng trong ví dụ cuối cùng của bạn, kết quả của tổng kết không được sử dụng và loại bỏ toàn bộ vòng lặp. In tổng sau khi vòng lặp sẽ tạo sự khác biệt. –

+0

Ý tưởng hay - điều đó chắc chắn làm chậm nó xuống. Mặc dù thành thật mà nói, trong kinh nghiệm của tôi gọi print() mà nhiều lần là cực kỳ chậm anyway, vì vậy thật khó để nói những gì khác biệt nó làm cho. Đó là một ví dụ điển hình về sự khác biệt tối ưu giữa hai phương thức - có vẻ như nó cũng nên đưa ra kết luận tương tự về vòng lặp reduce(). – hundley

+0

Có lẽ bài viết này có thể cung cấp một số thông tin chi tiết về sự khác biệt về hiệu suất giữa các vòng lặp và giảm: http://airspeedvelocity.net/2015/08/03/arrays-linked-lists-and-performance/ – JDS

Trả lời

24

Không phải phương pháp Mảng tích hợp sẽ nhanh hơn cách tiếp cận ngây thơ để thực hiện các thao tác như vậy? Có lẽ ai đó có kiến ​​thức cấp thấp hơn tôi có thể làm sáng tỏ tình hình.

Tôi chỉ muốn giải quyết phần này của câu hỏi và hơn thế nữa từ cấp độ khái niệm (ít hiểu về bản chất của trình tối ưu hóa của Swift trên phần của tôi) với "không nhất thiết". Nó đến từ nền tảng trong thiết kế trình biên dịch và kiến ​​trúc máy tính hơn là kiến ​​thức sâu về bản chất của trình tối ưu hóa của Swift.

Calling Overhead

Với các chức năng như mapreduce chức năng chấp nhận như đầu vào, nó đặt một căng thẳng hơn vào tôi ưu hoa để đặt nó một cách. Sự cám dỗ tự nhiên trong một trường hợp ngắn như vậy là liên tục chuyển đổi qua lại giữa việc thực hiện, ví dụ: map và đóng cửa bạn cung cấp, và tương tự như vậy truyền dữ liệu qua các nhánh mã khác nhau này (thông qua thanh ghi và ngăn xếp) , thông thường).

Loại chi phí gọi/phân nhánh này rất khó để trình tối ưu hóa loại bỏ, đặc biệt là do tính linh hoạt của các đóng cửa của Swift (không phải là không thể nhưng khá khó). Trình tối ưu hóa C++ có thể gọi hàm đối tượng nội tuyến nhưng với nhiều hạn chế và kỹ thuật tạo mã cần thiết để thực hiện nó, nơi trình biên dịch sẽ tạo ra một bộ hướng dẫn hoàn toàn mới cho map cho mỗi loại đối tượng hàm bạn truyền vào (và với trợ giúp rõ ràng của lập trình viên chỉ ra một mẫu chức năng được sử dụng để tạo mã).

Vì vậy, bạn không nên ngạc nhiên khi thấy rằng vòng tay được cuộn bằng tay của bạn có thể hoạt động nhanh hơn - chúng đặt rất ít sự căng thẳng vào trình tối ưu hóa. Tôi đã thấy một số người trích dẫn rằng những chức năng bậc cao này sẽ có thể đi nhanh hơn do nhà cung cấp có thể làm những việc như song song vòng lặp, nhưng để hiệu quả song song vòng lặp đầu tiên sẽ yêu cầu loại thông tin thường cho phép trình tối ưu hóa nội tuyến các cuộc gọi hàm lồng nhau bên trong đến một điểm mà chúng trở nên rẻ như các vòng được cuộn bằng tay. Nếu không, chức năng thực thi/đóng cửa mà bạn truyền vào sẽ mờ đục đối với các hàm như map/reduce: chúng chỉ có thể gọi nó và trả phí làm như vậy và không thể song song nó vì chúng không thể giả định bất kỳ điều gì về bản chất của các tác dụng phụ và thread-safety khi làm như vậy. Tất nhiên đây là tất cả khái niệm - Swift có thể tối ưu hóa những trường hợp này trong tương lai, hoặc bây giờ nó có thể có thể làm như vậy (xem -Ofast như một cách thường được trích dẫn để làm cho Swift đi nhanh hơn tại chi phí của một số an toàn).Nhưng nó sẽ đặt một sự căng thẳng nặng hơn vào trình tối ưu hóa, ít nhất, để sử dụng các chức năng này trên các vòng tay, và sự khác biệt về thời gian bạn thấy trong điểm chuẩn đầu tiên dường như phản ánh sự khác biệt mong đợi với chi phí gọi điện bổ sung này. Cách tốt nhất để tìm hiểu là xem xét lắp ráp và thử các cờ tối ưu hóa khác nhau.

Chức năng chuẩn

Đó không phải để ngăn cản việc sử dụng các chức năng như vậy. Họ làm rõ ràng hơn ý định thể hiện, họ có thể tăng năng suất. Và dựa vào chúng có thể cho phép codebase của bạn nhanh hơn trong các phiên bản tương lai của Swift mà không có bất kỳ sự tham gia nào từ phía bạn. Nhưng chúng không nhất thiết phải luôn nhanh hơn - đó là một quy tắc chung tốt để nghĩ rằng chức năng thư viện cấp cao hơn thể hiện trực tiếp hơn những gì bạn muốn làm sẽ nhanh hơn, nhưng luôn có những ngoại lệ đối với quy tắc (nhưng tốt nhất được phát hiện trong hindsight với một profiler trong tay vì nó là tốt hơn để sai lầm ở phía bên của sự tin tưởng hơn ngờ vực ở đây).

Artificial Benchmarks

Đối với điểm chuẩn thứ hai của bạn, nó là gần như chắc chắn là kết quả của trình biên dịch tối ưu hóa đi mã mà không có tác dụng phụ ảnh hưởng đến đầu ra dùng. Các tiêu chuẩn nhân tạo có khuynh hướng gây hiểu lầm là kết quả của những gì các trình tối ưu hóa làm để loại bỏ các tác dụng phụ không liên quan (tác dụng phụ không ảnh hưởng đến đầu ra của người dùng, về bản chất). Vì vậy, bạn phải cẩn thận khi xây dựng điểm chuẩn với thời gian có vẻ quá tốt là đúng rằng chúng không phải là kết quả của trình tối ưu hóa chỉ bỏ qua tất cả công việc mà bạn thực sự muốn đánh giá. Ít nhất, bạn muốn các bài kiểm tra của bạn xuất ra một số kết quả cuối cùng được thu thập từ tính toán.

+3

Điều này thực sự mang tính thông tin - tôi đã luôn luôn đọc rằng các hàm bậc cao sẽ nhanh hơn bằng cách song song các vòng lặp, nhưng bây giờ tôi thấy tại sao không phải lúc nào cũng như vậy. Là một câu hỏi liên quan - bạn có biết bất kỳ tài nguyên tốt nào để tối ưu hóa mã Swift không? Tôi quan tâm đến việc đạt được kiến ​​thức cấp thấp hơn để cải thiện các ứng dụng của mình. – hundley

+3

Sợ không - Tôi hầu như không phải là một chuyên gia về Swift. Ngoài các thuật toán và song song, tôi muốn đề xuất nhiều tài nguyên độc lập về ngôn ngữ hơn và xem xét kiến ​​trúc máy tính, thiết kế hướng dữ liệu, bố trí bộ nhớ và tối ưu hóa liên quan đến bộ nhớ cache. Những người nắm giữ bất kể ngôn ngữ, kể từ khi phần cứng là như nhau. Bạn đã có phần quan trọng nhất - một trình hồ sơ trong tay và khả năng đo lường chính xác và chính xác mã. Phần còn lại có lẽ là săn lùng những điểm nóng hàng đầu đó và tìm ra lý do tại sao chúng tồn tại và cách giải quyết chúng, và làm việc theo cách của bạn từ đó. –

12

Tôi không thể nói nhiều về thử nghiệm đầu tiên của bạn (map()append() trong vòng lặp) nhưng tôi có thể xác nhận kết quả của bạn. Vòng lặp nối tiếp trở nên nhanh hơn nếu bạn thêm

output.reserveCapacity(array.count) 

sau khi tạo mảng. Có vẻ như Apple có thể cải thiện mọi thứ ở đây và bạn có thể gửi báo cáo lỗi.

Trong

for _ in 0..<100_000 { 
    var sum: Float = 0 
    for element in array { 
     sum += element 
    } 
} 

trình biên dịch (có lẽ) loại bỏ toàn bộ vòng lặp vì kết quả tính toán không được sử dụng ở tất cả. Tôi chỉ có thể suy đoán lý do tại sao một tối ưu hóa tương tự không xảy ra trong

for _ in 0..<100_000 { 
    let sum = array.reduce(0, combine: {$0 + $1}) 
} 

nhưng nó sẽ khó khăn hơn để quyết định nếu gọi reduce() với việc đóng cửa có bất kỳ tác dụng phụ hay không.

Nếu mã kiểm tra được thay đổi một chút để tính toán và in một tổng

do { 
    var total = Float(0.0) 
    let start = NSDate() 
    for _ in 0..<100_000 { 
     total += array.reduce(0, combine: {$0 + $1}) 
    } 
    let elapsed = NSDate().timeIntervalSinceDate(start) 
    print("sum with reduce:", elapsed) 
    print(total) 
} 

do { 
    var total = Float(0.0) 
    let start = NSDate() 
    for _ in 0..<100_000 { 
     var sum = Float(0.0) 
     for element in array { 
      sum += element 
     } 
     total += sum 
    } 
    let elapsed = NSDate().timeIntervalSinceDate(start) 
    print("sum with loop:", elapsed) 
    print(total) 
} 

sau đó cả hai biến thể mất khoảng 10 giây trong thử nghiệm của tôi.

+4

+1 để chỉ ra khả năng dự trữ() - cải thiện tốc độ khoảng 3x. Từ những thử nghiệm này, có vẻ như reduce() có thể được tối ưu hóa hơn một chút so với map(). – hundley

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