2010-10-27 34 views
6

Có cách nào để tối ưu hóa dòng mã C sau đây (để tránh phân nhánh) không?Làm cách nào để tối ưu hóa dòng mã C này (phạm vi kiểm tra)?

if ((i < -threshold) || (i > threshold)) 
{ 
    counter++; 
} 

Tất cả các biến là số nguyên được ký 16 bit. Một phiên bản được tối ưu hóa nên có tính di động cao.

+5

bạn nói "cả hai" nhưng có ba biến. – McKay

+0

không thể nhớ nếu điều này làm việc chắc chắn, nhưng hãy thử 'if ((unsigned int) i> ngưỡng)' – zdav

+0

@zdav Nó chắc chắn không làm việc cho hầu hết các trình biên dịch. Các phôi như vậy ít nhất được thực hiện theo định nghĩa và thường nhận được sự bổ sung của bạn 2. –

Trả lời

12

Làm thế nào về:

counter += (i < -threshold) | (i > threshold); 

Giả sử mã ban đầu là hợp lệ, sau đó điều này nên làm việc quá, theo một cách cầm tay. Tiêu chuẩn cho biết rằng các toán tử quan hệ (<, > và vv) trả lại một int bằng 1 khi thành công hoặc 0 về lỗi.

CẬP NHẬT

Để trả lời bình luận Sheen dưới đây, đoạn mã sau:

int main() 
{ 
    short threshold = 10; 
    short i = 20; 
    short counter = 0; 

    counter += (i < -threshold) | (i > threshold); 

    return 0; 
} 

kết quả trong disassembler sau trên x86 sử dụng GCC, không có optimisations:

push %rbp 
    mov %rsp,%rbp 
    movw $0xa,-6(%rbp) 
    movw $0x14,-4(%rbp) 
    movw $0x0,-2(%rbp) 
    movswl -4(%rbp),%edx 
    movswl -6(%rbp),%eax 
    neg %eax 
    cmp %eax,%edx 
    setl %dl 
    movzwl -4(%rbp),%eax 
    cmp -6(%rbp),%ax 
    setg %al 
    or  %edx,%eax 
    movzbw %al,%dx 
    movzwl -2(%rbp),%eax 
    lea (%rdx,%rax,1),%eax 
    mov %ax,-2(%rbp) 
    mov $0x0,%eax 
    leaveq 
    retq 
+0

không hiểu cách ngăn chặn phân nhánh này. Bạn có thể dán mã lắp ráp được tạo ra ở đây không? – Sheen

+3

Điều này sẽ thêm 2 để truy cập nếu ngưỡng < 0, i > ngưỡng và i <-ngưỡng. Có thể an toàn khi giả sử rằng ngưỡng> = 0, nhưng nếu vậy OP sẽ chỉnh sửa để thêm giả định này. –

+0

@Sheen Trên x86, việc đánh giá các điều kiện như các số nguyên có thể được thực hiện với các lệnh 'setl' và' setg', một ít tốn kém vì không phổ biến nhưng vẫn rẻ hơn nhiều so với chi nhánh bị sai lệch. –

1

So sánh tuyệt đối của cả hai số

short imask = i >> sizeof(short) * 8 - 1; //compute the sign bit 1 or 0 
short tmask = threshold >> sizeof(short) * 8 - 1; //compute the sign bit 1 or 0 

short iabsolute = (i + imask)^imask; // compute i absolute 
short tabsolute = (threshold + tmask)^tmask; // compute threshold absolute 

counter += iabsolute > tabsolute; 
+0

Dịch chuyển số âm phải là UB. Người hỏi hỏi "di động". –

+0

Tốt. C99 có 'CHAR_BIT' trong limits.h thay vì' 8' để làm cho nó hoạt động trên kiến ​​trúc khác thường (nhưng vẫn là 2 bổ sung). Ngoài ra, bạn có thể sử dụng "tuyệt đối> ngưỡng", có thể. –

+2

@Oli Charlesworth Không, nó được thực hiện xác định. 6.5.7.5. –

1

Tùy thuộc vào việc phân phối giá trị của 'i', CPU của bạn có thể lưu bộ nhớ cache dự đoán chi nhánh cho bạn tốt hơn bất kỳ thay đổi mã nào bạn có thể thực hiện. Xem http://igoro.com/archive/fast-and-slow-if-statements-branch-prediction-in-modern-processors/ để có một ghi chú thú vị. Reddit thảo luận ở đây: http://www.reddit.com/r/programming/comments/c7ues/fast_and_slow_ifstatements_branch_prediction_in/

1

Bạn có thể sử dụng các thủ thuật sau đây giúp giảm các chi nhánh để chi nhánh duy nhất:

if (((unsigned) (i + threshold)) > (threshold << 1)) 
{ 
    counter++; 
} 

hoặc, đối với các pedantic:

if (((unsigned) i + (unsigned) threshold) > ((unsigned) threshold << 1)) 
{ 
    counter++; 
} 
+0

Việc bổ sung (và dịch chuyển trái) có thể tràn. Ngoài ra, sẽ chỉ có một nhánh trong mã ban đầu (tốt, phụ thuộc vào tập lệnh, tôi giả sử). –

+0

@Oli: Nó không thể tràn nếu ban đầu không tràn. Nếu sự dịch chuyển trái bị tràn thì thử nghiệm ban đầu '(i <-ngưỡng) || (i> ngưỡng) 'sẽ không có ý nghĩa. Những công việc này. Tôi đã sử dụng nó rất nhiều. Đó là một tinh chỉnh không rõ ràng. – Skizz

+0

@Skizz: Tôi đồng ý rằng điều này hoạt động trong thực tế trong số học bổ sung hai. Nhưng về mặt kỹ thuật, hành vi trên tràn số nguyên là không xác định. Và điều này có thể xảy ra trong mã của bạn nếu ví dụ: ngưỡng = 'INT_MAX'. –

1

này được dựa trên bit twiddling hacks, (được khuyến nghị)

#define CHAR_BIT 8 

int main() 
{ 
    int i=-3; // example input 
    int treshold=2; // example treshold 
    int count=0; 
    // step 1: find the absolute value of i 
    unsigned int r; // the result goes here 
    int const mask = i >> (sizeof(int) * CHAR_BIT - 1); 
    r = (i + mask)^mask; 
    // step 2: compute the sign of the difference 
    // sign becomes 0 (if r<=treshold) 
    // sign becomes 1 otherwise 
    int sign = 1^((unsigned int)(r-treshold-1) >> (sizeof(int) * CHAR_BIT - 1)); 
    count+=sign; 
    return count; 
} 

Làm việc này cho 32 bit số nguyên, thích ứng với 16 bit nên dễ dàng. Nó biên dịch bằng g ++.

Tốc độ phụ thuộc vào bộ xử lý đã sử dụng. Việc phân nhánh có thể nhanh hơn sau tất cả.

+1

Số âm chuyển dịch phải được xác định thực hiện. –

+0

Từ trang web hack twiddling hack: Vào ngày 7 tháng 3 năm 2003, Angus Duggan đã chỉ ra rằng đặc tả kỹ thuật ANSI C năm 1989 đã để lại kết quả của việc thực hiện đúng ca đã được xác định, vì vậy trên một số hệ thống, hack này có thể không hoạt động. Tôi đã đọc rằng ANSI C không yêu cầu các giá trị được biểu diễn dưới dạng bổ sung của hai, vì vậy nó có thể không hoạt động vì lý do đó (trên một số lượng nhỏ các máy cũ vẫn sử dụng một phần bổ sung). Vì vậy, nó phụ thuộc vào cách di động OP muốn câu hỏi được trả lời. – mirk

+2

@Oli, bạn nói đúng rằng các số âm chuyển dịch phải được thực hiện xác định. Nếu bạn tìm thấy một trình biên dịch không thực hiện điều này như là bản sao của các bit quan trọng (ví dụ như những gì mọi người mong đợi) Tôi sẽ gửi cho bạn một bootle của rượu .. (không, trình biên dịch được viết bởi chính mình không áp dụng) –

1

Oli Charlesworth, tôi nghĩ, có ý tưởng đúng. Tuy nhiên, tôi nghi ngờ rằng nó có thể được tối ưu hóa thêm (với chi phí dễ đọc).

Ngưỡng có thể được chuẩn hóa thành 0 để loại bỏ so sánh.

Đó là, ...

counter += ((unsigned) (i + threshhold) < (unsigned) (threshhold + threshhold)); 
+0

Một trong những bổ sung này có thể tràn. –

+0

Oli là đúng nhưng nó dễ dàng cố định. Truyền đến 'unsigned' trước khi thêm và sau đó nó là tốt. Vì các giá trị ban đầu phù hợp với 'int int', nó sẽ hoạt động tốt. –

+0

@R: Trên các hệ thống sử dụng số học bổ sung của hai, đúc một int âm để unsigned sẽ thêm (UINT_MAX + 1) vào nó, nhưng tôi tin tiêu chuẩn cho phép rõ ràng cho các hệ thống sử dụng định dạng dấu +. trừ giá trị từ ((UINT_MAX + 1)/2). Thật không may, tôi không biết về bất kỳ cách nào có thể được bảo đảm để thêm giá trị có thể âm vào giá trị chưa ký khi tổng có thể nằm giữa INT_MAX và UINT_MAX. – supercat

9

Có một thành ngữ tiêu chuẩn cho phạm vi kiểm tra với một hướng dẫn so sánh duy nhất.Nó đi như:

(unsigned)x - a <= (unsigned)b - a /* a <= x <= b */ 
(unsigned)x - a < (unsigned)b - a /* a <= x < b */ 

Là một ví dụ phổ biến (phiên bản này nếu isdigit là đảm bảo được đúng theo tiêu chuẩn):

(unsigned)ch - '0' < 10 

Nếu loại ban đầu của bạn là lớn hơn int (ví dụ long long) sau đó bạn sẽ cần phải sử dụng các loại chưa ký lớn hơn (ví dụ unsigned long long). Nếu ab là các hằng số hoặc đã có loại chưa ký hoặc nếu bạn biết b-a sẽ không tràn, bạn có thể bỏ qua đoạn trích từ b.

Để phương thức này hoạt động, tự nhiên bạn phải có a<=b và các loại/giá trị phải sao cho biểu thức gốc (ví dụ: a <= x && x <= b hoặc tương tự) hoạt động chính xác về mặt toán học. Ví dụ: nếu x được ký và b không được ký, x<=b có thể đánh giá sai khi x=-1b=UINT_MAX-1. Miễn là các loại ban đầu của bạn đều được ký hoặc nhỏ hơn loại chưa ký bạn đúc, đây không phải là vấn đề.

Đối với cách "lừa" này hoạt động, nó hoàn toàn xác định, sau khi giảm modulo UINT_MAX+1, cho dù x-a nằm trong khoảng 0 đến b-a.

Trong trường hợp của bạn, tôi nghĩ rằng những điều sau đây nên làm việc tốt:

(unsigned)i + threshold > 2U * threshold; 

Nếu threshold không thay đổi giữa lặp loop, trình biên dịch có lẽ có thể giữ cả threshold2U*threshold trong thanh ghi.

Nói về tối ưu hóa, trình biên dịch tốt nên tối ưu hóa thử nghiệm phạm vi ban đầu của bạn để sử dụng số học chưa ký, nơi nó biết các ràng buộc được đáp ứng. Tôi nghi ngờ nhiều người làm như vậy với ab không đổi, nhưng có lẽ không phải với các biểu thức phức tạp hơn. Ngay cả khi trình biên dịch có thể tối ưu hóa nó, mặc dù, thành ngữ (unsigned)x-a<b-a vẫn cực kỳ hữu ích trong các macro mà bạn muốn đảm bảo rằng x được đánh giá chính xác một lần.

+0

Đây là câu trả lời đúng IMO. Giống như http://stackoverflow.com/questions/17095324/fastest-way-in-c-to-determine-if-an-integer-is-between-two-integers-inclusive – netigger

3

Ồ, quá xấu câu hỏi đã được trả lời. Để diễn giải câu trả lời Oli của, mã

#include <stdint.h> 
int main() 
{ 
    int32_t threshold_square = 100; 
    int16_t i = 20; 
    int16_t counter = 0; 

    counter += ((int32_t) i * i > threshold_square); 

    return 0; 
} 

mang lại sự lắp ráp x86 sau sử dụng GCC mà không cần tối ưu hóa

pushq %rbp 
movq %rsp, %rbp 
movl $100, -8(%rbp) 
movw $20, -2(%rbp) 
movw $0, -4(%rbp) 
movswl -2(%rbp),%edx 
movswl -2(%rbp),%eax 
imull %edx, %eax 
cmpl -8(%rbp), %eax 
setg %al 
movzbl %al, %edx 
movzwl -4(%rbp), %eax 
leal (%rdx,%rax), %eax 
movw %ax, -4(%rbp) 
movl $0, %eax 
leave 
ret 

mà là bốn hướng dẫn ít hơn sử dụng (i < -threshold) | (i > threshold).

Cho dù điều này tốt hơn hay không, tất nhiên, tùy thuộc vào kiến ​​trúc.

(Việc sử dụng stdint.h là dành cho mục đích minh họa, cho C89 nghiêm ngặt thay thế với bất cứ điều gì có liên quan cho hệ thống đích.)

+0

+1: Tôi hoàn toàn không nghĩ về điều này. Nice (và trong tầm nhìn, rõ ràng) cách tiếp cận! –

+0

Nhiều như điều này là chính xác và tối ưu hơn phương pháp của Oli, một lợi thế của phương pháp của anh ta (và các biến thể của nó xuất hiện trong các câu trả lời khác) là nó dễ dàng mở rộng nó để kiểm tra phạm vi bất đối xứng, trong khi ở đây phạm vi luôn đối xứng. – ysap

-1

Điều gì là sai với mã gốc? Liệu nó có thực sự cần tối ưu hóa tay không?

Bất kỳ trình biên dịch nào cũng có thể tối ưu hóa điều đó rất tốt. Bất kỳ sự tối ưu hóa bằng tay nào cũng có thể chỉ dẫn đến sự làm xáo trộn.

1

Mã này không có nhánh nào có độ di động cao (tuy nhiên, việc triển khai abs có thể có một).

#include <stdlib.h> 
counter += abs(i) > threshold; 

Đó là biểu thức tuân thủ tiêu chuẩn đơn giản nhất.

Nếu trình biên dịch của bạn không sử dụng macro được tối ưu hóa cho abs(), bạn có thể sử dụng hàm macro/nội tuyến của riêng bạn.

Đó là những ví dụ, rằng việc sử dụng bản chất của định dạng twos bổ sung được sử dụng trên hầu hết các máy:

#define ABS(x) ((x)*(((x)>>15)|1)) 

#define ABS(x) ((x)-((x)>>15)^((x)>>15)) 

Ngoài ra bạn có thể thay thế toán tử so sánh với biểu hiện như thế này:

#define LESS(x, y) (-((x)-(y))>>15)) 

mã kết quả:

counter -= ((threshold - abs(i)) >> 15); 

Tất cả các macro đó dựa trên thực tế, dịch chuyển sang phải sang số của các bit trừ đi một giá trị dương hoặc giá trị 0 bằng không, và các giá trị âm đến một giá trị âm. Nhưng thực hiện thats được xác định.

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