2012-07-23 14 views
7

Tôi có một ứng dụng mà tôi phải tăng một số thống kê quầy trong một phương pháp đa luồng. Việc gia tăng phải an toàn cho luồng, vì vậy tôi đã quyết định sử dụng hàm dựng sẵn nguyên tử gcc __sync_add_and_fetch(). Chỉ để có được một ý tưởng về tác động của chúng, tôi đã thực hiện một số thử nghiệm hiệu năng đơn giản và nhận thấy rằng các hàm này chậm hơn nhiều so với việc tăng/giảm trước đơn giản.Việc xây dựng nguyên tử gcc có chậm đến mức bình thường không?

Đây là chương trình thử nghiệm mà tôi tạo ra:

#include <iostream> 
#include <pthread.h> 
#include <time.h> 

using namespace std; 

uint64_t diffTimes(struct timespec &start, struct timespec &end) 
{ 
    if(start.tv_sec == end.tv_sec) 
    { 
    return end.tv_nsec - start.tv_nsec; 
    } 
    else if(start.tv_sec < end.tv_sec) 
    { 
    uint64_t nsecs = (end.tv_sec - start.tv_sec) * 1000000000; 
    return nsecs + end.tv_nsec - start.tv_nsec; 
    } 
    else 
    { 
    // this is actually an error 
    return 0; 
    } 
} 

void outputResult(const char *msg, struct timespec &start, struct timespec &end, uint32_t numIterations, uint64_t val) 
{ 
    uint64_t diff = diffTimes(start, end); 
    cout << msg << ": " 
     << "\n\t iterations: " << numIterations 
     << ", result: " << val 
     << "\n\t times [start, end] = [" << start.tv_sec << ", " << start.tv_nsec << "]" 
     << "\n\t [" << end.tv_sec << ", " << end.tv_nsec << "]" 
     << "\n\t [total, avg] = [" << diff 
     << ", " << (diff/numIterations) << "] nano seconds" 
     << endl; 
} 

int main(int argc, char **argv) 
{ 
    struct timespec start, end; 
    uint64_t val = 0; 
    uint32_t numIterations = 1000000; 

    // 
    // NON ATOMIC pre increment 
    // 
    clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &start); 
    for(uint32_t i = 0; i < numIterations; ++i) 
    { 
    ++val; 
    } 
    clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &end); 

    outputResult("Non-Atomic pre-increment", start, end, numIterations, val); 
    val = 0; 

    // 
    // NON ATOMIC post increment 
    // 
    clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &start); 
    for(uint32_t i = 0; i < numIterations; ++i) 
    { 
    val++; 
    } 
    clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &end); 

    outputResult("Non-Atomic post-increment", start, end, numIterations, val); 
    val = 0; 

    // 
    // ATOMIC add and fetch 
    // 
    clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &start); 
    for(uint32_t i = 0; i < numIterations; ++i) 
    { 
    __sync_add_and_fetch(&val, 1); 
    } 
    clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &end); 

    outputResult("Atomic add and fetch", start, end, numIterations, val); 
    val = 0; 

    // 
    // ATOMIC fetch and add 
    // 
    clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &start); 
    for(uint32_t i = 0; i < numIterations; ++i) 
    { 
    __sync_fetch_and_add(&val, 1); 
    } 
    clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &end); 

    outputResult("Atomic fetch and add", start, end, numIterations, val); 
    val = 0; 

    // 
    // Mutex protected post-increment 
    // 
    pthread_mutex_t mutex; 
    pthread_mutex_init(&mutex, NULL); 
    clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &start); 
    for(uint32_t i = 0; i < numIterations; ++i) 
    { 
    pthread_mutex_lock(&mutex); 
    val++; 
    pthread_mutex_unlock(&mutex); 
    } 
    clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &end); 

    outputResult("Mutex post-increment", start, end, numIterations, val); 
    val = 0; 

    // 
    // RWlock protected post-increment 
    // 
    pthread_rwlock_t rwlock; 
    pthread_rwlock_init(&rwlock, NULL); 
    clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &start); 
    for(uint32_t i = 0; i < numIterations; ++i) 
    { 
    pthread_rwlock_wrlock(&rwlock); 
    val++; 
    pthread_rwlock_unlock(&rwlock); 
    } 
    clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &end); 

    outputResult("RWlock post-increment", start, end, numIterations, val); 
    val = 0; 

    return 0; 
} 

Và đây là kết quả:

# ./atomicVsNonAtomic 
Non-Atomic pre-increment: 
     iterations: 1000000, result: 1000000 
     times [start, end] = [0, 1585375] 
     [0, 1586185] 
     [total, avg] = [810, 0] nano seconds 
Non-Atomic post-increment: 
     iterations: 1000000, result: 1000000 
     times [start, end] = [0, 1667489] 
     [0, 1667920] 
     [total, avg] = [431, 0] nano seconds 
Atomic add and fetch: 
     iterations: 1000000, result: 1000000 
     times [start, end] = [0, 1682037] 
     [0, 16595016] 
     [total, avg] = [14912979, 14] nano seconds 
Atomic fetch and add: 
     iterations: 1000000, result: 1000000 
     times [start, end] = [0, 16617178] 
     [0, 31499571] 
     [total, avg] = [14882393, 14] nano seconds 
Mutex post-increment: 
     iterations: 1000000, result: 1000000 
     times [start, end] = [0, 31526810] 
     [0, 68515763] 
     [total, avg] = [36988953, 36] nano seconds 
RWlock post-increment: 
     iterations: 1000000, result: 1000000 
     times [start, end] = [0, 68547649] 
     [0, 110877351] 
     [total, avg] = [42329702, 42] nano seconds 

Dưới đây là tổng hợp gcc:

g++ -o atomicVsNonAtomic.o -c -march=i686 -O2 -I. atomicVsNonAtomic.cc 
g++ -o atomicVsNonAtomic atomicVsNonAtomic.o -lrt -lpthread 

Và thông tin liên quan và phiên bản:

# gcc --version 
gcc (GCC) 4.3.2 
Copyright (C) 2008 Free Software Foundation, Inc. 
This is free software; see the source for copying conditions. There is NO 
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 

# uname -a 
Linux gtcba2v1 2.6.32.12-0.7-default #1 SMP 2010-05-20 11:14:20 +0200 i686 i686 i386 GNU/Linux 

Và bây giờ cho câu hỏi thực tế :) Có bình thường là các hoạt động nguyên tử chậm hơn rất nhiều?

Sự khác biệt cho một triệu lần lặp là:

  • đơn giản sau increment: 431 nano giây
  • nguyên tử lấy và thêm hoạt động: 14.882.393 nano giây

Tất nhiên tôi hiểu rằng một hoạt động nguyên tử nên tốn kém hơn, nhưng điều này có vẻ phóng đại. Chỉ cần cho đầy đủ, tôi cũng đã kiểm tra một mutex pthread và rwlock. Ít nhất các hoạt động nguyên tử nhanh hơn sau đó các hoạt động pthread, nhưng Im vẫn tự hỏi nếu tôi đã làm điều gì đó sai. Tôi không thể làm cho nó liên kết mà không chỉ định tùy chọn -march=i686, có thể điều này có tác động không?

UPDATE:

Tôi đã diễn ra tối ưu hóa -O2 biên dịch và đã có thể có được kết quả chặt chẽ hơn như sau:

# ./atomicVsNonAtomic 
Non-Atomic pre-increment: 
     iterations: 1000000, result: 1000000 
     times [start, end] = [0, 1647303] 
     [0, 4171164] 
     [total, avg] = [2523861, 2] nano seconds 
Non-Atomic post-increment: 
     iterations: 1000000, result: 1000000 
     times [start, end] = [0, 4310230] 
     [0, 7262704] 
     [total, avg] = [2952474, 2] nano seconds 
Atomic add and fetch: 
     iterations: 1000000, result: 1000000 
     times [start, end] = [0, 7285996] 
     [0, 25919067] 
     [total, avg] = [18633071, 18] nano seconds 
Atomic fetch and add: 
     iterations: 1000000, result: 1000000 
     times [start, end] = [0, 25941677] 
     [0, 44544234] 
     [total, avg] = [18602557, 18] nano seconds 
Mutex post-increment: 
     iterations: 1000000, result: 1000000 
     times [start, end] = [0, 44573933] 
     [0, 82318615] 
     [total, avg] = [37744682, 37] nano seconds 
RWlock post-increment: 
     iterations: 1000000, result: 1000000 
     times [start, end] = [0, 82344866] 
     [0, 125124498] 
     [total, avg] = [42779632, 42] nano seconds 
+0

Dường như tối ưu hóa, ống lót hoặc bộ nhớ cache có liên quan. Thử thả tùy chọn tối ưu hóa trên g ++ – WiSaGaN

+1

Các hoạt động nguyên tử liên quan nhiều đến bộ đệm (chúng bằng cách nào đó bỏ qua bộ nhớ đệm). Hãy nhớ rằng truy cập bộ nhớ cache nhanh hơn hàng trăm lần truy cập vào bộ nhớ RAM của bạn. –

Trả lời

18

Câu trả lời là GCC tối ưu hóa từng bước phi nguyên tử của bạn xa. Khi nó thấy một vòng lặp như:

for (int i=0; i<N; i++) x++; 

nó thay thế nó bằng:

x += N; 

Điều này có thể được nhìn thấy trong hội tạo ra, trong đó có:

call clock_gettime 
leal -32(%ebp), %edx 
addl $1000000, -40(%ebp)  <- increment by 1000000 
adcl $0, -36(%ebp) 
movl %edx, 4(%esp) 
movl $2, (%esp) 
call clock_gettime 

Vì vậy, bạn không đo lường những gì bạn nghĩ rằng bạn đang có.

Bạn có thể tạo biến số volatile để tránh tối ưu hóa này. Trên máy tính của tôi, sau khi thực hiện điều này, truy cập không nguyên tử nhanh gấp 8 lần truy cập nguyên tử. Khi sử dụng biến 32 bit thay vì 64 bit (tôi biên dịch dưới dạng 32 bit), chênh lệch giảm xuống khoảng 3 lần.

+0

Tôi thậm chí không xem xét tối ưu hóa, vì tôi đã quá tập trung vào các ops nguyên tử. Tôi lấy ra -O2 và đạt được kết quả gần hơn nhiều. Tôi sẽ cập nhật câu hỏi gốc. Cảm ơn! – Brady

6

Tôi đoán rằng gcc là tối ưu hóa hoạt động tăng phi nguyên tử của bạn để một cái gì đó giống như

val += numIterations; 

Bạn nói rằng 10^6 gia số đang dùng 431 nano giây, mà làm việc ra 0,000431 ns mỗi vòng lặp. Trên một bộ xử lý 4 GHz, một chu kỳ đồng hồ là 0,25 ns, do đó, nó khá rõ ràng vòng lặp đang được tối ưu hóa đi. Điều này giải thích sự khác biệt hiệu suất lớn mà bạn đang thấy.

Chỉnh sửa: Bạn đã đo hoạt động nguyên tử khi lấy 14 ns - giả sử bộ xử lý 4 GHz một lần nữa, hoạt động với 56 chu kỳ, khá tốt!

+0

Tôi đã cập nhật câu hỏi ban đầu với kết quả sau khi xóa tối ưu hóa trình biên dịch. Tôi đồng ý rằng ban đầu 14 ns là khá phong nha, tôi đã chỉ tò mò tại sao có một sự khác biệt như vậy. Bây giờ tôi biết, cảm ơn :) – Brady

1

Không thể đo tốc độ chậm của bất kỳ cơ chế đồng bộ nào bằng một chuỗi đơn lẻ. Các đối tượng đồng bộ hóa một quá trình như các điểm ngắt của POSIX/Windows chỉ thực sự tốn thời gian khi chúng bị tranh chấp.

Bạn sẽ phải giới thiệu một số chủ đề - thực hiện công việc khác phản ánh thời gian ứng dụng thực tế của bạn- cho các phương pháp được đồng bộ hóa để đạt được ý tưởng thực sự về thời gian thực hiện.

+0

Đồng ý, thử nghiệm của tôi đã không xem xét bất kỳ loại khóa ganh đua, thay vì thời gian tham gia vào hoạt động nguyên tử đơn giản và khóa/mở khóa. Thời gian trong thử nghiệm của tôi là đường cơ sở cho một kịch bản nhiều luồng, sẽ giống nhau hoặc chậm hơn. Ban đầu tôi đã quan tâm để biết sự khác biệt giữa một gia tăng đơn giản và tương đương nguyên tử của nó. Tôi chỉ cần thêm những thứ pthread để thấy sự khác biệt với các ops nguyên tử. Tôi sẽ thử nó đa luồng bây giờ, cảm ơn. – Brady