2013-06-16 28 views
6

Tôi đang chơi xung quanh với std::thread và một cái gì đó kỳ lạ xuất hiện bất ngờ:2 luồng chậm hơn 1?

#include <thread> 

int k = 0; 

int main() { 
    std::thread t1([]() { while (k < 1000000000) { k = k + 1; }}); 
    std::thread t2([]() { while (k < 1000000000) { k = k + 1; }}); 

    t1.join(); 
    t2.join(); 

    return 0; 
} 

Khi biên dịch mã trên bằng không tối ưu hóa sử dụng kêu vang ++, tôi có các tiêu chuẩn sau đây:

real 0m2.377s 
user 0m4.688s 
sys 0m0.005s 

sau đó tôi đã thay đổi mã của tôi như sau: (Hiện chỉ sử dụng 1 chuỗi)

#include <thread> 

int k = 0; 

int main() { 
    std::thread t1([]() { while (k < 1000000000) { k = k + 1; }}); 

    t1.join(); 

    return 0; 
} 

Và đây là các tiêu chuẩn mới:

real 0m2.304s 
user 0m2.298s 
sys 0m0.003s 

Tại sao mã sử dụng 2 luồng chậm hơn mã sử dụng 1?

+3

Tất cả các chủ đề đang làm là bashing đầu cố gắng để đọc từ và ghi vào k. Và khi lần đầu tiên kết thúc, nó vẫn phải đợi đến lần thứ hai. – chris

+0

@chris: 'k' không phải là' biến động', do đó, các chuỗi không cạnh tranh, bởi vì chúng không chia sẻ hiệu quả 'k'. –

+1

@MooingDuck: 'volatile' đảm bảo ghi vào bộ nhớ, nhưng không có' biến động' không ngăn cản chúng. Câu hỏi cụ thể nói "... không có tối ưu hóa ..." và nó là điển hình cho các bản dựng không được tối ưu hóa để làm theo các hướng dẫn chương trình rất nghĩa đen và không phải để "cache" giá trị trong sổ đăng ký. Trên phần cứng Intel/AMD gần đây, có khả năng xả tự động giữa các bộ đệm lõi truy cập cùng một địa chỉ bộ nhớ sẽ làm chậm quá trình này. –

Trả lời

15

Bạn có hai chuỗi chiến đấu trên cùng một biến, k. Vì vậy, bạn đang dành thời gian mà các bộ vi xử lý nói "Bộ xử lý 1: Này, bạn có biết giá trị nào k có? Bộ xử lý 2: Chắc chắn, ở đây bạn đi!", Ping-ponging qua lại vài lần cập nhật. Vì k không phải là nguyên tử, cũng không đảm bảo rằng thread2 không ghi giá trị "cũ" là k để chuỗi thời gian tiếp theo 1 đọc giá trị, nó nhảy ngược lại 1, 2, 10 hoặc 100 bước và phải thực hiện nó trên một lần nữa - trong lý thuyết mà có thể dẫn đến không phải của các vòng hoàn thiện, nhưng điều đó sẽ đòi hỏi khá nhiều may mắn.

+0

Điều này có ý nghĩa. Cảm ơn bạn. –

+7

@ Magtheridon96: Ngoài những gì Mats nói, chương trình của bạn có một cuộc đua dữ liệu và do đó hành vi chính thức không xác định. _Anything_ có thể xảy ra bởi vì bạn truy cập một biến không phải nguyên tử được chia sẻ từ hai luồng mà không đồng bộ hóa. Mô hình bộ nhớ của bộ vi xử lý x86 là hơi khoan dung nhưng bạn thực sự thực sự không nên dựa vào đó. – JohannesD

+0

@JohannesD Lúc đầu, tôi đã thử nghiệm nó với một 'std :: nguyên tử ', và tôi có kết quả tương tự, vì vậy tôi đã thử với một 'int'. Cả hai testcases đều nhanh hơn, nhưng tỷ số giữa các lần đều giống nhau. Cảm ơn các bạn. Bạn đã cho tôi thấy tầm quan trọng của việc có các đơn vị lưu trữ nguyên tử. –

4

Điều này thực sự là một nhận xét để trả lời câu trả lời của Mats Petersson, nhưng tôi muốn cung cấp các ví dụ về mã.

Sự cố là tranh chấp của một tài nguyên cụ thể và cũng là một đường dẫn trong bộ nhớ cache.

Alternative 1:

#include <cstdint> 
#include <thread> 
#include <vector> 
#include <stdlib.h> 

static const uint64_t ITERATIONS = 10000000000ULL; 

int main(int argc, const char** argv) 
{ 
    size_t numThreads = 1; 
    if (argc > 1) { 
     numThreads = strtoul(argv[1], NULL, 10); 
     if (numThreads == 0) 
      return -1; 
    } 

    std::vector<std::thread> threads; 

    uint64_t k = 0; 
    for (size_t t = 0; t < numThreads; ++t) { 
     threads.emplace_back([&k]() { // capture k by reference so we all use the same k. 
      while (k < ITERATIONS) { 
       k++; 
      } 
     }); 
    } 

    for (size_t t = 0; t < numThreads; ++t) { 
     threads[t].join(); 
    } 
    return 0; 
} 

Đây là chủ đề tranh cho một biến duy nhất, thực hiện cả hai đọc và viết mà buộc nó ping-pong gây tranh cãi và đưa ra luận cứ luồng duy nhất hiệu quả nhất.

#include <cstdint> 
#include <thread> 
#include <vector> 
#include <stdlib.h> 
#include <atomic> 

static const uint64_t ITERATIONS = 10000000000ULL; 

int main(int argc, const char** argv) 
{ 
    size_t numThreads = 1; 
    if (argc > 1) { 
     numThreads = strtoul(argv[1], NULL, 10); 
     if (numThreads == 0) 
      return -1; 
    } 

    std::vector<std::thread> threads; 

    std::atomic<uint64_t> k = 0; 
    for (size_t t = 0; t < numThreads; ++t) { 
     threads.emplace_back([&]() { 
      // Imperfect division of labor, we'll fall short in some cases. 
      for (size_t i = 0; i < ITERATIONS/numThreads; ++i) { 
       k++; 
      } 
     }); 
    } 

    for (size_t t = 0; t < numThreads; ++t) { 
     threads[t].join(); 
    } 
    return 0; 
} 

Ở đây chúng tôi phân chia lao động theo định thức (chúng tôi thuộc trường hợp numThread không phải là ước số của ITERATIONS nhưng đủ gần để trình diễn). Thật không may, chúng tôi vẫn gặp phải tranh chấp để truy cập vào phần tử được chia sẻ trong bộ nhớ.

#include <cstdint> 
#include <thread> 
#include <vector> 
#include <stdlib.h> 
#include <atomic> 

static const uint64_t ITERATIONS = 10000000000ULL; 

int main(int argc, const char** argv) 
{ 
    size_t numThreads = 1; 
    if (argc > 1) { 
     numThreads = strtoul(argv[1], NULL, 10); 
     if (numThreads == 0) 
      return -1; 
    } 

    std::vector<std::thread> threads; 
    std::vector<uint64_t> ks; 

    for (size_t t = 0; t < numThreads; ++t) { 
     threads.emplace_back([=, &ks]() { 
      auto& k = ks[t]; 
      // Imperfect division of labor, we'll fall short in some cases. 
      for (size_t i = 0; i < ITERATIONS/numThreads; ++i) { 
       k++; 
      } 
     }); 
    } 

    uint64_t k = 0; 
    for (size_t t = 0; t < numThreads; ++t) { 
     threads[t].join(); 
     k += ks[t]; 
    } 
    return 0; 
} 

Một lần nữa điều này là xác định về phân phối khối lượng công việc và chúng tôi dành một chút nỗ lực để kết hợp kết quả. Tuy nhiên chúng tôi đã không làm gì để đảm bảo việc phân phối các quầy ủng hộ phân phối CPU lành mạnh. Đối với điều đó:

#include <cstdint> 
#include <thread> 
#include <vector> 
#include <stdlib.h> 

static const uint64_t ITERATIONS = 10000000000ULL; 
#define CACHE_LINE_SIZE 128 

int main(int argc, const char** argv) 
{ 
    size_t numThreads = 1; 
    if (argc > 1) { 
     numThreads = strtoul(argv[1], NULL, 10); 
     if (numThreads == 0) 
      return -1; 
    } 

    std::vector<std::thread> threads; 
    std::mutex kMutex; 
    uint64_t k = 0; 

    for (size_t t = 0; t < numThreads; ++t) { 
     threads.emplace_back([=, &k]() { 
      alignas(CACHE_LINE_SIZE) uint64_t myK = 0; 
      // Imperfect division of labor, we'll fall short in some cases. 
      for (uint64_t i = 0; i < ITERATIONS/numThreads; ++i) { 
       myK++; 
      } 
      kMutex.lock(); 
      k += myK; 
      kMutex.unlock(); 
     }); 
    } 

    for (size_t t = 0; t < numThreads; ++t) { 
     threads[t].join(); 
    } 
    return 0; 
} 

Ở đây chúng tôi tránh tranh chấp giữa các luồng xuống cấp độ bộ nhớ cache, ngoại trừ trường hợp chúng tôi sử dụng mutex để kiểm soát đồng bộ hóa. Đối với khối lượng công việc tầm thường này, mutex sẽ có một địa ngục có giá tương đối. Ngoài ra, bạn có thể sử dụng alignas để cung cấp mỗi thread với bộ nhớ riêng của nó ở phạm vi bên ngoài và tóm tắt kết quả sau khi kết nối, loại bỏ sự cần thiết cho mutex. Tôi để nó như một bài tập cho người đọc.

+0

Cảm ơn bạn đã dành thời gian để làm tất cả điều này :). Tôi thích biến thể thứ 3. –

+0

Hãy thử thứ 3 với đối tượng là alignas (128) (hoặc bất kỳ kích thước dòng bộ nhớ cache của bạn) - mục tiêu là di chuyển không gian làm việc của luồng tới vị trí không có * bất kỳ * trùng với những CPU khác đang hoạt động trên. – kfsone

+0

Cuối cùng tại một máy tính thay vì điện thoại, sửa lỗi trình biên dịch. – kfsone

2

Dường như với tôi giống như câu hỏi quan trọng hơn "tại sao điều này không hoạt động?" là "Làm cách nào để làm việc này?"Đối với các nhiệm vụ trong tầm tay, tôi nghĩ std::async (mặc dù significant shortcomings) thực sự là một công cụ tốt hơn so với sử dụng std::thread trực tiếp.

#include <future> 
#include <iostream> 

int k = 0; 
unsigned tasks = std::thread::hardware_concurrency(); 
unsigned reps = 1000000000/tasks; 

int main() { 
    std::vector<std::future<int>> f; 

    for (int i=0; i<tasks; i++) 
     f.emplace_back(std::async(std::launch::async, 
            [](){int j; for (j=0; j<reps; j++); return j;}) 
        ); 

    for (int i=0; i<tasks; i++) { 
     f[i].wait(); 
     k += f[i].get(); 
    } 

    std::cout << k << "\n"; 
    return 0; 
} 
+0

Bạn là người đàn ông tốt bụng Jerry. –

0

Tôi chạy vào vấn đề này. Ý kiến ​​của tôi là đối với loại nhất định của việc chi phí quản lý chủ đề có thể có nhiều hơn những lợi ích bạn nhận được từ chạy trong chủ đề. đây là code ví dụ của tôi, làm một số công việc thực tế ở một số vòng lặp lớn các lần lặp lại, vì vậy tôi có số lượng rất phù hợp với các lệnh thời gian.

pair<int,int> result{0,0}; 
#ifdef USETHREAD 
     thread thread_l(&Myclass::trimLeft, this, std::ref(fsq), std::ref(oriencnt), std::ref(result.first)); 
     thread thread_r(&Myclass::trimRight, this, std::ref(fsq), std::ref(oriencnt), std::ref(result.second)); 
     thread_l.join(); 
     thread_r.join(); 
#else 
     // non threaded version faster 
     trimLeft(fsq, oriencnt, result.first); 
     trimRight(fsq, oriencnt, result.second); 
#endif 

    return result; 

Kết quả thời gian

Thead   No_thread 
===========================  
Real 4m28s   2m49s 
usr 0m55s   2m49s 
sys 0m6.2s   0m0.012s 

Tôi bỏ qua số thập phân trong vài giây cho số lớn. Mã của tôi chỉ cập nhật một biến được chia sẻ oriencnt. Tôi chưa cho phép cập nhật fsq. Có vẻ như trong phiên bản luồng, hệ thống đang làm nhiều công việc hơn dẫn đến thời gian đồng hồ dài hơn (thời gian thực). Cờ trình biên dịch của tôi là mặc định -g -O2, không chắc đây là vấn đề chính hay không. Khi biên dịch với -O3 sự khác biệt là tối thiểu. Ngoài ra còn có một số hoạt động IO kiểm soát mutex. Thử nghiệm của tôi cho thấy rằng điều này không góp phần vào sự khác biệt. Tôi đang sử dụng gcc 5.4 với C++ 11. Một khả năng là thư viện không được tối ưu hóa.

đây được biên soạn với O3

 Thead No_thread 
========================= 
real 4m24  2m44s 
usr 0m54s  2m44s 
sys 0m6.2s  0m0.016s 
Các vấn đề liên quan