2014-09-24 10 views
10

Để khởi tạo chủ đề lười biếng, nên một biến tĩnh trong một hàm, std :: call_once hoặc khóa kiểm tra kép rõ ràng? Có bất kỳ sự khác biệt có ý nghĩa nào không?Khởi tạo lười biếng theo chủ đề: static vs std :: call_once vs khóa được kiểm tra đôi

Tất cả ba có thể được nhìn thấy trong câu hỏi này.

Double-Checked Lock Singleton in C++11

Hai phiên bản của đôi checked khóa trong C++ 11 lượt lên trong Google.

Anthony Williams shows cả khóa được kiểm tra kép với thứ tự bộ nhớ rõ ràng và tiêu chuẩn :: call_once. Ông không đề cập đến tĩnh nhưng bài viết đó có thể đã được viết trước khi trình biên dịch C++ 11 có sẵn.

Jeff Preshing, trong một rộng rãi writeup, mô tả một số biến thể của khóa đã kiểm tra kép. Anh ta đề cập đến việc sử dụng một biến tĩnh như một tùy chọn và thậm chí anh ta còn cho thấy rằng các trình biên dịch sẽ tạo ra mã cho khóa được kiểm tra kép để khởi tạo một biến tĩnh. Nó không rõ ràng với tôi nếu ông kết luận rằng một cách là tốt hơn so với khác.

Tôi hiểu được rằng cả hai bài viết đều có ý nghĩa sư phạm và không có lý do để làm điều này. Trình biên dịch sẽ làm điều đó cho bạn nếu bạn sử dụng một biến tĩnh hoặc std :: call_once.

+1

Hãy cảnh báo rằng VC++ đang chạy phía sau về chức năng an toàn thread địa phương thống kê. Họ không có trong VS2013. Nhưng được báo cáo là trong VS2014: http://blogs.msdn.com/b/vcblog/archive/2014/06/11/c-11-14-feature-tables-for-visual-studio-14-ctp1. aspx –

+1

Mặt khác, GCC có thể làm cho statics địa phương nhanh hơn call_once hoặc kiểm tra kép bởi vì nó có thể sử dụng các thủ thuật nền tảng cụ thể để tránh bất kỳ hoạt động nguyên tử nào cả. –

+1

@CortAmmon Nếu bạn đăng câu trả lời đó bằng một số bằng chứng tôi chấp nhận. – Praxeolitic

Trả lời

15

GCC sử dụng các thủ thuật nền tảng cụ thể để tránh các hoạt động nguyên tử hoàn toàn trên đường dẫn nhanh, tận dụng thực tế là nó có thể phân tích static tốt hơn so với call_once hoặc kiểm tra kép.

Vì kiểm tra kép sử dụng nguyên tử làm phương pháp tránh trường hợp đua, nên phải trả giá mỗi lần mua. Nó không phải là một mức giá cao, nhưng đó là một mức giá.

Nó phải trả tiền này bởi vì nguyên tử phải duy trì nguyên tử trong mọi trường hợp, thậm chí các hoạt động khó như trao đổi so sánh. Điều này làm cho nó rất khó để tối ưu hóa. Nói chung, trình biên dịch phải để nó trong, chỉ trong trường hợp bạn sử dụng biến cho nhiều hơn chỉ là một khóa đôi. Nó không có cách nào dễ dàng chứng minh rằng bạn không bao giờ sử dụng một trong những hoạt động phức tạp hơn trên nguyên tử của bạn.

Mặt khác, static là chuyên môn cao và là một phần của ngôn ngữ. Nó được thiết kế, ngay từ đầu, rất dễ khởi tạo. Theo đó, trình biên dịch có thể thực hiện các lối tắt không có sẵn cho phiên bản chung hơn. The compiler actually emits đoạn mã sau cho một tĩnh:

một chức năng đơn giản:

void foo() { 
    static X x; 
} 

được viết lại bên trong GCC để:

void foo() { 
    static X x; 
    static guard x_is_initialized; 
    if (__cxa_guard_acquire(x_is_initialized)) { 
     X::X(); 
     x_is_initialized = true; 
     __cxa_guard_release(x_is_initialized); 
    } 
} 

nào trông rất giống một khóa kiểm tra lại. Tuy nhiên, trình biên dịch được ăn gian một chút ở đây. Nó biết người dùng không bao giờ có thể viết trực tiếp bằng cách sử dụng trực tiếp cxa_guard. Nó biết rằng nó chỉ được sử dụng trong các trường hợp đặc biệt mà trình biên dịch chọn sử dụng nó. Do đó, với thông tin bổ sung đó, nó có thể tiết kiệm thời gian. Các thông số kỹ thuật bảo vệ CXA, như được phân phối như chúng, tất cả chia sẻ common rule: __cxa_guard_acquire sẽ không bao giờ sửa đổi byte đầu tiên của bộ bảo vệ và __cxa_guard__release sẽ đặt nó thành khác 0.

Điều này có nghĩa là mỗi người bảo vệ phải đơn điệu, và nó chỉ định chính xác những hoạt động sẽ làm như thế.Theo đó nó có thể tận dụng lợi thế của bảo vệ trường hợp chủng tộc hiện có trong nền tảng máy chủ. Ví dụ, trên x86, bảo vệ LL/SS được bảo đảm bởi các CPU được đồng bộ hóa mạnh trở thành đủ để thực hiện mẫu lấy/giải phóng này, vì vậy nó có thể thực hiện raw đọc byte đầu tiên đó khi nó khóa kép, thay vì đọc. Điều này chỉ có thể bởi vì GCC không sử dụng API nguyên tử C++ để thực hiện khóa kép của nó - nó đang sử dụng một platform specific approach.

GCC không thể tối ưu hóa nguyên tử trong trường hợp chung. Trên các kiến ​​trúc được thiết kế để được đồng bộ hóa ít hơn (chẳng hạn như được thiết kế cho 1024 lõi), GCC không dựa vào kiến ​​trúc để làm LL/SS cho nó. Vì vậy GCC buộc phải thực sự phát ra nguyên tử. Tuy nhiên, trên các nền tảng phổ biến như x86 và x64, nó có thể nhanh hơn.

call_once có thể có hiệu quả thống kê của GCC, vì nó giới hạn tương tự số lượng hoạt động có thể được thực hiện cho once_flag đến một phần của các hàm có thể được áp dụng cho nguyên tử. Sự cân bằng là các statics thuận tiện hơn nhiều khi sử dụng, khi chúng được áp dụng, nhưng call_once hoạt động trong nhiều trường hợp không có đủ số liệu thống kê (chẳng hạn như once_flag thuộc sở hữu của một đối tượng được tạo động).

Có sự khác biệt nhỏ về hiệu suất giữa tĩnh và call_once trên các nền tảng cao hơn này. Nhiều người trong số các nền tảng này, trong khi không cung cấp LL/SS, ít nhất sẽ cung cấp đọc không rách của một số nguyên. Các nền tảng này có thể sử dụng điều này và một con trỏ cụ thể theo chuỗi, để thực hiện per-thread epoch counting to avoid atomics. Điều này là đủ cho tĩnh hoặc call_once, nhưng phụ thuộc vào quầy không lăn qua. Nếu bạn không có số nguyên 64 bit không bị rách, call_once phải lo lắng về việc di chuột qua. Việc thực hiện có thể hoặc không thể lo lắng về điều này. Nếu nó bỏ qua vấn đề này, nó có thể nhanh như statics. Nếu nó chú ý đến vấn đề đó, nó phải chậm như nguyên tử. Tĩnh biết tại thời gian biên dịch bao nhiêu biến tĩnh/khối có, do đó, nó có thể chứng minh không có rollover tại thời gian biên dịch (hoặc ít nhất là darn tự tin!)

+0

Tôi nhận ra tôi đã trễ vài năm để có câu trả lời tuyệt vời này. Đối với những người không được khởi xướng, bạn có ý nghĩa gì với "bảo vệ LL/SS"? – fbrereto

+0

@fbrereto LL/SS là "Load Load/Store Store" Nó nói rằng hai tải của một adddress bộ nhớ được đảm bảo xảy ra theo thứ tự, và hai cửa hàng của một địa chỉ bộ nhớ được đảm bảo xảy ra theo thứ tự. Tuy nhiên, sự đảm bảo này không nói bất cứ điều gì về thứ tự của các tải trọng và các cửa hàng liên quan đến nhau. –

+0

Cảm ơn bạn đã làm rõ. Một câu hỏi khác: Tôi đã thử tải ví dụ 'foo' của bạn ở trên vào https://godbolt.org/, và theo GCC, tôi thấy cuộc gọi có được xảy ra trước khi so sánh. Đây có phải là dự kiến ​​không? https://godbolt.org/g/oV65PL – fbrereto

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