2011-01-24 34 views
7

Hôm nay tôi đọc mã bằng cách sử dụng bảng tra cứu thay vì nếu khác để cắt hai giá trị uint8 tổng hợp. Bản đồ là tôi trong số i={0...255} và 255 trong số i={256...511}. Tôi tự hỏi mức độ lớn của lợi ích này có thể là gì và cố gắng tìm ra, sử dụng gprof,Bảng tìm kiếm so với if-else

g++ -std=c++0x -pg perfLookup.cpp -O2 -o perfLookup && ./perfLookup && gprof perfLookup |less 

với mã được đính kèm bên dưới. Bây giờ không có cờ gzip -O2 nói rằng tra cứu() mất 45% và ifelse() như 48% thời gian thực hiện. Với -O2 mặc dù nó là 56% cho tra cứu() và 43% cho ifelse(). Nhưng điểm chuẩn này có đúng không? Có lẽ rất nhiều mã được tối ưu hóa đi từ dst là không bao giờ đọc?

#include <iostream> 
#include <cstdint> 
#include <vector> 

void lookup(std::vector<uint8_t> src, int repeat) { 
    uint8_t lookup[511]; 
    for (int i = 0; i < 256; i++) { 
    lookup[i] = i; 
    } 
    for (int i = 256; i < 512; i++) { 
    lookup[i] = 255; 
    } 

    std::vector<uint8_t> dst(src.size()); 
    for (int i = 0; i < repeat; i++) { 
    for (int i = 0; i < src.size(); i++) { 
     dst[i] = lookup[src[i]]; 
    } 
    } 

} 

void ifelse(std::vector<uint8_t> src, int repeat) { 
    std::vector<uint8_t> dst(src.size()); 
    for (int i = 0; i < repeat; i++) { 
    for (int i = 0; i < src.size(); i++) { 
     dst[i] = (src[i] > 255) ? 255 : src[i]; 
    } 
    } 
} 

int main() 
{ 
    int n = 10000; 
    std::vector<uint8_t> src(n); 
    for (int i = 0; i < src.size(); i++) { 
    src[i] = rand() % 510; 
    } 

    lookup(src, 10000); 
    ifelse(src, 10000); 
} 

đang Cập nhật:

#include <iostream> 
#include <cstdint> 
#include <cstring> 
#include <vector> 
#include <algorithm> 

// g++ -std=c++0x -pg perfLookup.cpp -O2 -o perfLookup && ./perfLookup && gprof perfLookup |less 

std::vector<uint16_t> lookup(std::vector<uint16_t> src, int repeat) { 
    uint16_t lookup[511]; 
    for (int i = 0; i < 256; i++) { 
    lookup[i] = i; 
    } 
    for (int i = 256; i < 511; i++) { 
    lookup[i] = 255; 
    } 

    std::vector<uint16_t> dst(src.size()); 
    for (int i = 0; i < repeat; i++) { 
    for (int k = 0; k < src.size(); k++) { 
     dst[k] = lookup[src[k]]; 
    } 
    } 

    return dst; 

} 

std::vector<uint16_t> ifelse(std::vector<uint16_t> src, int repeat) { 
    std::vector<uint16_t> dst(src.size()); 
    for (int i = 0; i < repeat; i++) { 
    for (int k = 0; k < src.size(); k++) { 
     dst[k] = (src[k] > 255) ? 255 : src[k]; 
    } 
    } 
    return dst; 
} 

std::vector<uint16_t> copyv(std::vector<uint16_t> src, int repeat) { 
    std::vector<uint16_t> dst(src.size()); 
    for (int i = 0; i < repeat; i++) { 
    dst = src; 
    for (int k = 0; k < src.size(); k++) { 
     if (dst[k] > 255) { 
    dst[k] = 255; 
     } 
    } 
    } 
    return dst; 
} 

std::vector<uint16_t> copyC(std::vector<uint16_t> src, int repeat) 
{ 
    uint16_t* dst = (uint16_t *) malloc(sizeof(uint16_t) * src.size()); // Alloc array for dst 

    for (int i = 0; i < repeat; i++) { 
    std::memcpy(dst, &src[0], sizeof(uint16_t) * src.size()); // copy src into array 

    for (int k = 0; k < src.size(); k++) { 
     if ((dst[k] & 0xFF00) != 0) 
    dst[k] = 0x00FF; 
    } 
    } 

    free(dst); 
    return std::vector<uint16_t>(); 
} 

int main() 
{ 
    int n = 10000; 
    std::vector<uint16_t> src(n); 
    for (int i = 0; i < src.size(); i++) { 
    src[i] = rand() % 510; 
    } 
    std::vector<uint16_t> dst; 
    dst = lookup(src, 10000); 
    dst = ifelse(src, 10000); 
    dst = copyv(src, 10000); 
} 
+3

Lưu ý rằng bạn đang đo lường việc khởi tạo bảng tra cứu như một phần của điểm chuẩn. Thông thường bạn khởi tạo một bảng tra cứu một cách riêng biệt và không inlcude nó trong điểm chuẩn. –

+0

Tôi sẽ không bao gồm việc khởi tạo bảng tra cứu vào hàm được mô phỏng vì điều này có thể được thực hiện chỉ một lần trong khi thực hiện chương trình. –

+1

Một số thay đổi tôi sẽ thực hiện đối với mã: Sử dụng đối số 'src' và thực hiện việc cắt tại chỗ - lưu ý rằng đây là bản sao, không phải là tham chiếu đến bản gốc. Trả về vectơ đó từ hàm, nếu không thì trình biên dịch cũng có thể loại bỏ tất cả các mã từ các hàm vì biến cục bộ không bao giờ được sử dụng. Tạo và lưu trữ bảng tra cứu bên ngoài mã kiểm tra - không thêm các thao tác sẽ không ảnh hưởng đến kết quả. –

Trả lời

7

Vâng, kể từ src được khai báo là std::vector<uint8_t>, src[i]bao giờ lớn hơn 255, đó là giá trị cao nhất có thể cho một số nguyên unsigned 8-bit.

Vì vậy, tôi đoán là trình biên dịch tối ưu hóa việc kiểm tra. Những gì còn lại chỉ là vòng lặp boilerplate nên điểm chuẩn là vô nghĩa.

Cung cấp séc không vô nghĩa (ví dụ: kiểm tra so với 64 chứ không phải 255), kết quả của 'tối ưu hóa' có thể phụ thuộc rất nhiều vào máy. Dự đoán nhánh có thể (tùy thuộc vào dữ liệu đầu vào) làm tốt công việc giảm chi phí của chi nhánh. Bảng tra cứu theo yêu cầu khác (tùy thuộc vào truy cập bộ nhớ ngẫu nhiên dữ liệu đầu vào) và lưu trữ bộ nhớ cache ...

+0

Điểm tốt! Sau khi thay đổi mọi thứ thành uint16_t ifelse() là 58% và tra cứu() là 42%. Vì vậy, thực sự trình biên dịch đủ thông minh. (Tại -O3 nó thậm chí tối ưu hóa cả hai cuộc gọi chức năng, nhiều hơn hoặc ít hơn.) Phân phối vẫn ổn định khi thay đổi số vòng lặp, liên quan đến việc xây dựng bảng tra cứu. Tôi tự hỏi làm thế nào điều này có thể trông giống như trong hệ thống «thực» (hiệu ứng chỉnh sửa video, rất nhiều truy cập bộ nhớ cache khác) ... –

2

Bạn cũng đang đo thời gian khởi tạo bảng tra cứu và điều này có thể không là những gì bạn muốn. Nếu bảng chỉ được khởi tạo một lần trong mã sản xuất, nhưng được sử dụng nhiều lần, thì bạn không nên đo khởi tạo.

+0

Đồng ý, tôi sẽ khởi tạo bảng tra cứu ra khỏi hàm 'tra cứu '. Bạn thậm chí có thể mã hóa khởi tạo của nó và làm cho nó const (ví dụ: tra cứu const uint8_t [] = {0, 1, 2, 3 ...}) – GrahamS

+0

Tôi đồng ý; Trong trường hợp này xây dựng LUT là đủ nhanh mặc dù hầu như không có hiệu ứng nào (đặc biệt là khi lặp nhiều lần). –

7

Ngoài các điều Alexander đã nói:

bảng Lookup có thể cải thiện hiệu suấtmạnh. Tuy nhiên, điều này được bù đắp bởi thời gian cần thiết để tạo bảng tra cứu ngay từ đầu. Thông thường bạn sẽ điểm chuẩn riêng này.

Một điều khác cần lưu ý là bảng tra cứu yêu cầu không gian trong bộ nhớ cache và do đó có thể dẫn đến bộ nhớ cache bị bỏ sót nếu nó lớn. Nếu có đủ bộ nhớ cache, phương thức if sẽ nhanh hơn bảng tra cứu.

Cuối cùng, gprof rất tốt để xác định tắc nghẽn. Nhưng tôi sẽ không sử dụng nó cho điểm chuẩn. Thay vào đó, hãy sử dụng hàm thời gian. gprof sử dụng lấy mẫu mà có thể nói đúng là được ánh xạ tới thời gian tiêu thụ, nhưng ít chính xác hơn ở đây.

+0

Cảm ơn lời khuyên! Về chức năng thời gian, bạn đang nghĩ gì ở đây? Đồng hồ treo tường thời gian? Hoặc chu kỳ CPU? –

+0

@Simon: nghĩa là không quan trọng, khi bạn thực hiện đủ số lần lặp lại (nghĩa là độ dài chu kỳ cá nhân> 1 giây) và đủ lặp lại thử nghiệm. Nói chung, độ phân giải càng tốt thì kết quả càng tốt. Nhưng với tất cả các yếu tố khác luôn ảnh hưởng đến điểm chuẩn, không đặt quá nhiều tầm quan trọng vào đồng hồ. –

3

Việc xử lý mảng lookup bị hỏng. Dòng này:

uint8_t lookup[511]; 

bị tắt bởi một, bạn muốn lookup[512]; vì bạn dường như mong đợi lập chỉ mục với 511 (truy cập phần tử thứ 512). Tất nhiên, như Alexander đã chỉ ra, nó là tất cả tranh luận anyway kể từ uint8_t có nghĩa là bạn không thể có một chỉ mục của bất cứ điều gì trên 255.

Vì nó là, mã này:

for (int i = 256; i < 512; i++) { 
    lookup[i] = 255; 
} 

chí chỉ số nằm ngoài giới hạn và viết 255 đến một vị trí bộ nhớ nhiều hơn hoặc ít được lựa chọn một cách ngẫu nhiên.

+0

Cảm ơn, đúng vậy. Nó có nghĩa là tôi <511 ở đó. 511 không nên xảy ra vì tôi đang thêm hai số 8 bit, số tối đa là 255 + 255 = 510, tức là 511 kết quả có thể. –

+0

@Simon: chắc chắn xảy ra, tôi đã thêm mã không thành công. – unwind

+0

Vâng, do đó «i <511»;) Tôi đã phạm sai lầm với giới hạn trên đó. –

1

Đôi khi trình biên dịch đủ thông minh để tối ưu hóa các thử nghiệm lược tả đơn giản. Nếu đây là trường hợp bạn có lừa trình biên dịch không tối ưu hóa. Sử dụng một giá trị lặp lại lớn hơn nhiều cũng có thể giúp cung cấp cho bạn kết quả tốt hơn hoặc cho bạn biết nếu một cái gì đó đang được tối ưu hóa đi.

Bảng tìm kiếm có thể nhanh hơn chuỗi nếu/elseifs nhưng trong trường hợp này chỉ với một so sánh, tôi sẽ không mong đợi nhiều sự khác biệt. Ví dụ, nếu bạn có 10, 100, 1000 ... so sánh bảng tra cứu nói chung nên giành chiến thắng.

+0

Anh ấy đang thực hiện 10000 so sánh. – GrahamS

+0

Tôi có nghĩa là có nhiều so sánh trong cùng một khối nếu ... elseif (nghĩa là, một nếu theo sau là 1000 giá trị khác). – uesp

2

Cả hai cách tiếp cận dường như khá kỳ quặc. Bạn có thực sự cần mức tối ưu hóa này không? Nếu vậy thì tôi sẽ đặt câu hỏi về việc sử dụng các vectơ và xem xét các mảng C thay thế!

Cách tiếp cận "ifelse" có vẻ hiển nhiên hơn. Tôi nghi ngờ nó là đáng chú ý là chậm hơn/nhanh hơn so với bảng tra cứu trừ khi bạn đang gọi hàng tỷ lần này.

Cá nhân tôi có lẽ chỉ cần sao chép các vector src sau đó lặp trên nó và sửa chữa các giá trị (sử dụng 250 ở đây vì 255 làm cho không có cảm giác như chỉ ra):

std::vector<uint8_t> dst(src); 
for(std::vector<int>::size_type i = 0; i != v.size(); i++) 
{ 
    if (dst[i] > 250) dst[i] = 250; 
} 

Tùy thuộc vào cách nhân bản thực sự thực hiện và được tối ưu hóa bởi trình biên dịch (ví dụ: nó có thể làm một bản sao bộ nhớ khối) thì điều này thực sự có thể nhanh hơn một chút. Nó chắc chắn là dễ hiểu hơn và dễ hiểu hơn.

+0

Một cách tiếp cận tương tự sẽ được dành chỗ cho vector thứ hai - mà không cần sao chép thực tế, và sau đó 'std :: transform (v.begin(), v.end(), [] (uint8_t x) {return std :: min (x, 250)}); '(sử dụng cú pháp lambda cho nhỏ gọn-Ness) –

+0

Đúng, mức này là bắt buộc. Hiệu ứng video → mỗi phần trăm đếm :) Tôi đã được thông báo rằng truy cập mảng cũng không kém phần truy cập vectơ. Tôi đã cố gắng nhân bản, nhưng nó có vẻ chậm hơn nhiều (trừ khi tôi đang làm điều gì đó sai); nó so sánh với 80% với hàm copyv(), 12% cho ifelse() và 8% cho tra cứu(). Sẽ thêm mã được cập nhật trong câu hỏi ban đầu. –

+0

@David tôi có thể sử dụng cú pháp Lambda trong mã giống như trong ví dụ của bạn hay là cái gật đầu hợp lệ C++ này? (Tôi đã cố gắng và có được lỗi trình biên dịch) –

1

Một bẩn giải pháp C ít càng tốt (ra khỏi đỉnh đầu của tôi và chưa được kiểm tra/uncompiled, vì vậy có thể chứa sai lầm):

std::vector<uint16_t> copyC(std::vector<uint16_t> src, int repeat) 
{ 
    uint16_t* dst = malloc(sizeof(unit16_t) * src.size()); // Alloc array for dst 

    for (int i = 0; i < repeat; i++) 
    { 
     memcpy(dst, &src[0], sizeof(unit16_t) * src.size()); // copy src into array 

     for (int k = 0; k < src.size(); k++) 
     { 
      if ((dst[k] & 0xFF00) != 0) 
       dst[k] = 0x00FF; 
     } 
    } 

    free(dst); 
} 

Tôi muốn được quan tâm để xem làm thế nào mà so sánh. (Một lần nữa nó có thể phụ thuộc vào việc thực hiện memcpy, vì nó sẽ chỉ nhanh hơn nếu các bản sao bộ nhớ lớn hiệu quả hơn các bản sao byte-by-byte).

Tùy thuộc vào thông số kỹ thuật của chip (tức là kích thước đăng ký 8 bit hoặc 16 bit), truy cập một byte có thể nhanh hơn byte kép. nếu vậy thì đoạn mã trên cũng có thể được viết lại để xử lý dst như một mảng của unit8_t. Sau đó, nó sẽ chỉ kiểm tra từng byte thứ hai và nếu nó không khác được đặt là 0 và byte sau * thành 0xFF.

(* hoặc byte trước đó, tùy thuộc vào độ cuối)

+0

44,4% cho copyv, 43,9% cho copyC, 6,7% cho ifelse, 5% cho tra cứu (tất cả được thực hiện liên tiếp). Biên dịch với lặp lại -O2, 100k. Nếu tôi thay đổi mã copyv để sử dụng & 0xFF00 cũng nhanh hơn khoảng 1%. Vì vậy, tốc độ giữa hai thứ đó dường như bằng nhau. –

+0

@Simon A. Eugster: Một kết quả thú vị khác. Có vẻ như memcpy trong 'copyC' và bản sao vector trong' copyV' đang giết chết hiệu suất của chúng. Không quan tâm đến vector src' này lớn đến mức nào và nền tảng nào bạn đang chạy trên (chip và OS)? Cũng không sử dụng '& 0xFF00' tăng tốc độ giải pháp ifelse của bạn? – GrahamS

+0

(Oh xin lỗi, tôi đã không di chuyển đủ xa. Tôi thấy src là 10.000 * uint16_t) – GrahamS

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