2011-12-04 22 views
22

Tôi có .NET và C++ thực hiện chức năng kiểm tra hoàn hảo, thực hiện tra cứu 854,750 trong từ điển bằng các phím chuỗi từ một nhóm 6838 phím. Tôi đã viết những chức năng này để điều tra một nút cổ chai hoàn hảo trong một ứng dụng thực tế.C++ ~ 1M tra cứu trong unordered_map với chuỗi khóa hoạt động chậm hơn nhiều so với mã .NET

.NET thực hiện được viết bằng F #, sử dụng từ điển và được biên soạn cho .NET 4.0

C++ thực hiện sử dụng std :: unordered_map và được xây dựng với VS2010 trong chế độ Release.

Trên máy tính của tôi. Mã .NET chạy trong 240 ms trung bình và mã C++ chạy trong 630 mili giây. Bạn có thể vui lòng giúp tôi hiểu những gì có thể là lý do cho sự khác biệt lớn về tốc độ này?

Nếu tôi thực hiện độ dài khóa trong C++ ngắn hơn và sử dụng tiền tố "key_" thay vì "key_prefix_", nó sẽ chạy trong 140 ms.

Một thủ thuật khác mà tôi đã thử là thay thế std :: string bằng cách triển khai chuỗi không thay đổi tùy chỉnh có con trỏ const char * đến nguồn và băm được tính một lần. Sử dụng chuỗi này cho phép để có được hiệu suất của C++ thực hiện xuống 190 ms.

C++:

struct SomeData 
{ 
public: 
    float Value; 
}; 

typedef std::string KeyString; 
typedef std::unordered_map<KeyString, SomeData> DictionaryT; 

const int MaxNumberOfRuns = 125; 
const int MaxNumberOfKeys = 6838; 

DictionaryT dictionary; 
dictionary.rehash(MaxNumberOfKeys); 

auto timer = Stopwatch::StartNew(); 

int lookupCount = 0; 

char keyBuffer[100] = "key_prefix_"; 
size_t keyPrefixLen = std::strlen(keyBuffer); 

/// run MaxNumberOfRuns * MaxNumberOfKeys iterations 
for(int runId = 0; runId < MaxNumberOfRuns; runId++) 
{ 
    for(int keyId = 0; keyId < MaxNumberOfKeys; keyId++) 
    { 
     /// get a new key from the pool of MaxNumberOfKeys keys   
     int randomKeySuffix = (std::rand() % MaxNumberOfKeys); 
     ::itoa(randomKeySuffix, keyBuffer + keyPrefixLen, 10); 

     KeyString key = keyBuffer; 

     /// lookup key in the dictionary   
     auto dataIter = dictionary.find(key); 
     SomeData* data; 

     if(dataIter != dictionary.end()) 
     { 
      /// get existing value   
      data = &dataIter->second; 
     } 
     else 
     { 
      /// add a new value 
      data = &dictionary.insert(dataIter, DictionaryT::value_type(key, SomeData()))->second; 
     } 

     /// update corresponding value in the dictionary 
     data->Value += keyId * runId; 
     lookupCount++; 
    } 
} 

timer.Stop(); 
std::cout << "Time: " << timer.GetElapsedMilleseconds() << " ms" << std::endl; 
std::cout << "Lookup count: " << lookupCount << std::endl; 

Prints:

Thời gian: 636 ms
Lookup đếm: 854750

F # mã

open System 
open System.Diagnostics 
open System.Collections.Generic 

type SomeData = 
    struct 
     val mutable Value : float 
    end 

let dictionary = new Dictionary<string, SomeData>() 
let randomGen = new Random() 

let MaxNumberOfRuns = 125 
let MaxNumberOfKeys = 6838 

let timer = Stopwatch.StartNew() 

let mutable lookupCount = 0 

/// run MaxNumberOfRuns * MaxNumberOfKeys iterations 
for runId in 1 .. MaxNumberOfRuns do 
    for keyId in 1 .. MaxNumberOfKeys do 

     /// get a new key from the pool of MaxNumberOfKeys keys 
     let randomKeySuffix = randomGen.Next(0, MaxNumberOfKeys).ToString()   
     let key = "key_prefix_" + randomKeySuffix 

     /// lookup key in the dictionary 
     let mutable found, someData = dictionary.TryGetValue (key) 
     if not(found) then 
      /// add a new value 
      someData <- new SomeData() 
      dictionary.[key] <- someData 

     /// update corresponding value in the dictionary 
     someData.Value <- someData.Value + float(keyId) * float(runId) 

     lookupCount <- lookupCount + 1 

timer.Stop() 

printfn "Time: %d ms" timer.ElapsedMilliseconds 
printfn "Lookup count: %d" lookupCount 

Prints:

Thời gian: 245 ms
Lookup đếm: 854750

+2

Có lẽ sử dụng trình lặp kết thúc unordered_map vì gợi ý phần tử tiếp theo đang cản trở hiệu suất? – GWW

+1

Cả hai đều chạy từ bộ nhớ cache lạnh hoặc cả từ bộ nhớ cache nóng? – sarnold

+0

Không loại bỏ gợi ý phần tử tiếp theo không giúp: (Cả hai chức năng đều chạy với bộ đệm lạnh. – evgenyp

Trả lời

43

Visual Studio 2010 sử dụng một hàm băm performant cho std::string, chứ không phải là một chính xác. Về cơ bản, nếu chuỗi khóa lớn hơn 10 ký tự, hàm băm sẽ ngừng sử dụng mọi ký tự cho băm và có một bước tiến lớn hơn 1.

size_t operator()(const _Kty& _Keyval) const 
    { // hash _Keyval to size_t value by pseudorandomizing transform 
    size_t _Val = 2166136261U; 
    size_t _First = 0; 
    size_t _Last = _Keyval.size(); 
    size_t _Stride = 1 + _Last/10; 

    for(; _First < _Last; _First += _Stride) 
     _Val = 16777619U * _Val^(size_t)_Keyval[_First]; 
    return (_Val); 
    } 
  • size() >= 10 - sử dụng mỗi nhân vật thứ hai sau khi người đầu tiên
  • size() >= 20 - sử dụng mỗi nhân vật thứ ba sau khi người đầu tiên
  • ...

Nhờ vậy, sự va chạm xảy ra thường xuyên hơn, điều này làm chậm mã xuống. Hãy thử một hàm băm tùy chỉnh cho phiên bản C++.

+3

Wow, đó là một quan sát tuyệt vời Cảm ơn bạn – karunski

+5

Cảm ơn bạn Tôi xấu hổ rằng tôi đã không thử nó trước khi gửi câu hỏi này. chức năng C++ perf là ​​khoảng 180ms.Đó là ~ 50 ms nhanh hơn .NET – evgenyp

+0

Thú vị nếu tôi thực hiện một khóa lâu hơn một chút thì hiệu suất của cả hai C++ và .NET thực hiện sẽ gần như giống nhau (~ 260ms trên máy của tôi). Nếu tôi sử dụng các phím số nguyên so với C++ sẽ chạy trong ~ 35 ms và .NET trong ~ 40 ms. Có lẽ tôi sẽ tạo một .NET thực hiện toàn bộ hàm từ ứng dụng thực mà tôi chạy vòng lặp này (nó thực hiện một số dữ liệu xoa bóp nd tính toán sản phẩm chấm của nhiều vectơ) để xem hiệu suất của nó có thể so sánh với việc triển khai thực hiện như thế nào. – evgenyp

5

Chúng tôi chỉ có thể suy đoán tại sao một phiên bản nhanh hơn phiên bản kia. Bạn chắc chắn có thể sử dụng một hồ sơ ở đây để cho bạn biết nơi có điểm nóng. Vì vậy, không có bất kỳ điều này như là một câu trả lời dứt khoát.

lưu ý của bạn về C++ phiên bản là nhanh hơn với chiều dài khóa ngắn được chiếu sáng bởi vì nó có thể trỏ đến một vài điều:

  • Có lẽ hàm băm cho std :: string thực sự là tối ưu hóa cho các chuỗi nhỏ chứ không phải là chuỗi dài.
  • Có thể chuỗi dài hơn mất nhiều thời gian hơn để sao chép vào unordered_set vì nó vô hiệu hóa tối ưu hóa chuỗi nhỏ trong thư viện VS 2010 C++. Do đó, bản sao vào bản đồ yêu cầu phân bổ.

Tắt dải, đây là một số quan sát hoang dã dựa trên kinh nghiệm của tôi với unordered_map (mặc dù tôi quen thuộc hơn với việc triển khai tăng cường của Microsoft).

  • Trong ví dụ này, không có lý do gì để sử dụng chuỗi std :: làm loại khóa, chỉ cần sử dụng giá trị số nguyên. Điều này có lẽ sẽ làm cho cả hai phiên bản C++ và F # nhanh hơn.

  • Khi bạn chèn các giá trị vào bản đồ, có thể không nhanh hơn để tìm kiếm theo sau là chèn vì cả hai sẽ yêu cầu băm lại chuỗi khóa. Chỉ cần sử dụng toán tử [], thao tác tìm hoặc chèn trên nó là của riêng nó. Tôi đoán điều này sẽ phụ thuộc vào tần suất bạn tìm thấy một lần truy cập trong bản đồ so với việc thêm một giá trị mới.

  • Nếu phân bổ là nút cổ chai và bạn phải sử dụng loại khóa chuỗi, bạn có thể có hiệu suất tốt hơn trong việc lưu trữ ptr được chia sẻ vào chuỗi thay vì sao chép chuỗi khi bạn chèn nó vào bản đồ.

  • Cố gắng cung cấp hàm băm của riêng bạn cho các loại hình quan trọng mà bỏ qua "key_prefix_" một phần của chuỗi thực hiện

  • Cố gắng thúc đẩy của; có lẽ nó nhanh hơn.

Một lần nữa, chạy tiểu sử sẽ nhanh chóng cho bạn biết nơi cần tìm loại sự cố này. Cụ thể, nó sẽ cho bạn biết nếu có một nút cổ chai trong băm và một nút cổ chai trong phân bổ.

+0

Cảm ơn bạn. Tôi đã thử một số đề xuất của bạn. Tôi sẽ cố gắng chạy nó theo profiler.Tôi đã thất vọng vì tôi đã viết rằng phương pháp trong C + + ngây thơ mong đợi rằng C + + thực hiện sẽ đánh bại một NET ra khỏi hộp và sau đó tôi sẽ điều chỉnh nó làm cho nó chạy nhanh hơn. – evgenyp

+1

Trên tối ưu hóa chuỗi nhỏ: Thay vì làm suy giảm, nó sẽ cải thiện hiệu suất, vì tối ưu hóa chuỗi nhỏ cần phải sao chép tất cả các ký tự. Nếu nó không phải là chuỗi nhỏ, nó chỉ có thể trao đổi con trỏ, hoặc di chuyển chuỗi (VS2010 đã được di chuyển nhận thức). – Xeo

+0

Có các phím số nguyên hoạt động nhanh hơn rất nhiều. Tôi đã sử dụng chuỗi vì đây là những gì tôi có trong ứng dụng thực của mình. Tôi ước tôi có thể sử dụng các số nguyên ở đó, nhưng ngay bây giờ điều đó là không thể. – evgenyp

0

Khi bạn xử lý mã cấu trúc dữ liệu thuần túy, tỷ lệ tốc độ 2.6 không phải là lạ. Hãy xem slides on this project để xem ý tôi là gì.

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