Cách nhanh nhất để xác định xem một số nguyên có nằm giữa hai số nguyên (đã bao gồm) với các bộ giá trị đã biết không


389

Có cách nào nhanh hơn x >= start && x <= endtrong C hoặc C ++ để kiểm tra xem một số nguyên nằm giữa hai số nguyên không?

CẬP NHẬT : Nền tảng cụ thể của tôi là iOS. Đây là một phần của chức năng làm mờ hộp hạn chế pixel vào một vòng tròn trong một hình vuông nhất định.

CẬP NHẬT : Sau khi thử câu trả lời được chấp nhận , tôi nhận được một lệnh tăng tốc độ lớn trên một dòng mã so với thực hiện x >= start && x <= endtheo cách thông thường .

CẬP NHẬT : Đây là mã sau và trước với trình biên dịch mã từ XCode:

CÁCH MỚI

// diff = (end - start) + 1
#define POINT_IN_RANGE_AND_INCREMENT(p, range) ((p++ - range.start) < range.diff)

Ltmp1313:
 ldr    r0, [sp, #176] @ 4-byte Reload
 ldr    r1, [sp, #164] @ 4-byte Reload
 ldr    r0, [r0]
 ldr    r1, [r1]
 sub.w  r0, r9, r0
 cmp    r0, r1
 blo    LBB44_30

CÁCH CŨ

#define POINT_IN_RANGE_AND_INCREMENT(p, range) (p <= range.end && p++ >= range.start)

Ltmp1301:
 ldr    r1, [sp, #172] @ 4-byte Reload
 ldr    r1, [r1]
 cmp    r0, r1
 bls    LBB44_32
 mov    r6, r0
 b      LBB44_33
LBB44_32:
 ldr    r1, [sp, #188] @ 4-byte Reload
 adds   r6, r0, #1
Ltmp1302:
 ldr    r1, [r1]
 cmp    r0, r1
 bhs    LBB44_36

Khá tuyệt vời làm thế nào giảm hoặc loại bỏ phân nhánh có thể cung cấp một tốc độ đáng kể như vậy.


28
Tại sao bạn lo ngại rằng điều này không đủ nhanh cho bạn?
Matt Ball

90
Ai quan tâm tại sao, đó là một câu hỏi thú vị. Nó chỉ là một thách thức vì lợi ích của một thách thức.
David nói phục hồi Monica

46
@SLaks Vì vậy, chúng ta chỉ nên bỏ qua tất cả các câu hỏi như vậy một cách mù quáng và chỉ cần nói "hãy để trình tối ưu hóa làm điều đó?"
David nói Phục hồi Monica

87
nó không quan trọng tại sao câu hỏi đang được hỏi. Đó là một câu hỏi hợp lệ, ngay cả khi câu trả lời là không
tay10r

41
Đây là một nút cổ chai trong một chức năng trong một trong các ứng dụng của tôi
jjxtra

Câu trả lời:


527

Có một mẹo cũ để làm điều này chỉ với một so sánh / chi nhánh. Cho dù nó thực sự sẽ cải thiện tốc độ có thể được mở ra để đặt câu hỏi, và thậm chí nếu có, có lẽ quá ít để ý hoặc quan tâm, nhưng khi bạn chỉ bắt đầu với hai so sánh, cơ hội cải thiện rất lớn là rất xa. Mã này trông như:

// use a < for an inclusive lower bound and exclusive upper bound
// use <= for an inclusive lower bound and inclusive upper bound
// alternatively, if the upper bound is inclusive and you can pre-calculate
//  upper-lower, simply add + 1 to upper-lower and use the < operator.
    if ((unsigned)(number-lower) <= (upper-lower))
        in_range(number);

Với một máy tính hiện đại, điển hình (nghĩa là bất cứ thứ gì sử dụng bổ sung twos), việc chuyển đổi thành không dấu thực sự là một bước tiến - chỉ là một thay đổi trong cách xem các bit giống nhau.

Lưu ý rằng trong trường hợp điển hình, bạn có thể tính toán trước upper-lowerbên ngoài một vòng lặp (giả định), do đó thường không đóng góp bất kỳ thời gian đáng kể nào. Cùng với việc giảm số lượng hướng dẫn chi nhánh, điều này cũng (nói chung) cải thiện dự đoán chi nhánh. Trong trường hợp này, cùng một nhánh được thực hiện cho dù số nằm dưới đầu dưới hoặc trên đầu cuối của phạm vi.

Về cách thức hoạt động của nó, ý tưởng cơ bản khá đơn giản: một số âm, khi được xem là một số không dấu, sẽ lớn hơn bất cứ thứ gì bắt đầu là một số dương.

Trong thực tế phương pháp này dịch numbervà khoảng thời gian đến điểm gốc và kiểm tra nếu numbertrong khoảng [0, D], ở đâu D = upper - lower. Nếu numberdưới giới hạn dưới: âm và nếu trên giới hạn trên: lớn hơnD .


8
@ TomásBadan: Cả hai sẽ là một chu kỳ trên bất kỳ máy hợp lý nào. Những gì đắt tiền là chi nhánh.
Oliver Charlesworth

3
Phân nhánh bổ sung được thực hiện do ngắn mạch? Nếu đây là trường hợp, liệu lower <= x & x <= upper(thay vì lower <= x && x <= upper) sẽ dẫn đến hiệu suất tốt hơn?
Markus Mayr

6
Tài khoản @ Đối với tất cả những gì chúng ta biết, trình biên dịch của OP có thể hiển thị mã của OP bằng một mã opcode nhánh duy nhất ...
Oliver Charlesworth

152
Ôi !!! Điều này dẫn đến một thứ tự cải thiện cường độ trong ứng dụng của tôi cho dòng mã cụ thể này. Bằng cách tính toán trước mức thấp hơn, hồ sơ của tôi đã tăng từ 25% thời gian của chức năng này xuống dưới 2%! Hiện tại, nút cổ chai đang hoạt động cộng và trừ, nhưng tôi nghĩ nó có thể đủ tốt bây giờ :)
jjxtra

28
À, bây giờ @PologistsoDad đã cập nhật câu hỏi, rõ ràng tại sao điều này nhanh hơn. Các thực mã có một tác dụng phụ trong việc so sánh, đó là lý do trình biên dịch không thể tối ưu hóa việc ngắn mạch đi.
Oliver Charlesworth

17

Thật hiếm khi có thể thực hiện tối ưu hóa đáng kể để mã hóa ở quy mô nhỏ như vậy. Hiệu suất lớn đến từ việc quan sát và sửa đổi mã từ cấp cao hơn. Bạn có thể loại bỏ hoàn toàn nhu cầu kiểm tra phạm vi hoặc chỉ thực hiện O (n) trong số chúng thay vì O (n ^ 2). Bạn có thể sắp xếp lại các bài kiểm tra để luôn luôn ngụ ý một mặt của bất đẳng thức. Ngay cả khi thuật toán là lý tưởng, mức tăng có nhiều khả năng sẽ đến khi bạn thấy cách mã này kiểm tra phạm vi 10 triệu lần và bạn tìm cách gộp chúng lại và sử dụng SSE để thực hiện nhiều thử nghiệm song song.


16
Mặc dù các câu trả lời tôi đứng trước câu trả lời của tôi: Tập hợp được tạo ra (xem liên kết pastebin trong một bình luận cho câu trả lời được chấp nhận) là khá khủng khiếp đối với một cái gì đó trong vòng lặp bên trong của chức năng xử lý pixel. Câu trả lời được chấp nhận là một mẹo gọn gàng nhưng hiệu quả rõ rệt của nó vượt xa những gì hợp lý để mong đợi để loại bỏ một phần của một nhánh trên mỗi lần lặp. Một số hiệu ứng phụ đang chiếm ưu thế và tôi vẫn hy vọng rằng một nỗ lực để tối ưu hóa toàn bộ quá trình trong thử nghiệm này sẽ để lại lợi ích của việc so sánh phạm vi thông minh trong bụi.
Ben Jackson

17

Nó phụ thuộc vào số lần bạn muốn thực hiện kiểm tra trên cùng một dữ liệu.

Nếu bạn đang thực hiện kiểm tra một lần duy nhất, có lẽ không có cách nào có ý nghĩa để tăng tốc thuật toán.

Nếu bạn đang làm điều này cho một tập hợp các giá trị rất hữu hạn, thì bạn có thể tạo một bảng tra cứu. Việc thực hiện lập chỉ mục có thể tốn kém hơn, nhưng nếu bạn có thể điều chỉnh toàn bộ bảng trong bộ đệm, thì bạn có thể xóa tất cả các nhánh khỏi mã, điều này sẽ tăng tốc mọi thứ.

Đối với dữ liệu của bạn, bảng tra cứu sẽ là 128 ^ 3 = 2.097.152. Nếu bạn có thể điều khiển một trong ba biến để bạn xem xét tất cả các trường hợp start = Ntại một thời điểm, thì kích thước của tập làm việc giảm xuống 128^2 = 16432byte, phù hợp với hầu hết các bộ đệm hiện đại.

Bạn vẫn sẽ phải điểm chuẩn mã thực tế để xem liệu bảng tra cứu không phân nhánh có đủ nhanh hơn so với các so sánh rõ ràng hay không.


Vì vậy, bạn sẽ lưu trữ một số loại tra cứu được đưa ra một giá trị, bắt đầu và kết thúc và nó sẽ chứa một BOOL cho bạn biết nếu nó ở giữa?
jjxtra

Chính xác. Nó sẽ là một bảng tra cứu 3D : bool between[start][end][x]. Nếu bạn biết mẫu truy cập của bạn sẽ trông như thế nào (ví dụ x đang tăng đơn điệu), bạn có thể thiết kế bảng để bảo toàn cục bộ ngay cả khi toàn bộ bảng không vừa trong bộ nhớ.
Andrew Prock

Tôi sẽ xem liệu tôi có thể đi xung quanh để thử phương pháp này không và xem nó diễn ra như thế nào. Tôi dự định thực hiện nó với một vectơ bit trên mỗi dòng trong đó bit sẽ được đặt nếu điểm nằm trong vòng tròn. Nghĩ rằng sẽ nhanh hơn một byte hoặc int32 so với mặt nạ bit?
jjxtra

2

Câu trả lời này là để báo cáo về một thử nghiệm được thực hiện với câu trả lời được chấp nhận. Tôi đã thực hiện một bài kiểm tra phạm vi kín trên một vectơ lớn của số nguyên ngẫu nhiên được sắp xếp và thật ngạc nhiên, phương pháp cơ bản của (thấp <= num && num <= high) trên thực tế nhanh hơn câu trả lời được chấp nhận ở trên! Thử nghiệm đã được thực hiện trên HP Pavilion g6 (AMD A6-3400APU với ram 6GB. Đây là mã cốt lõi được sử dụng để thử nghiệm:

int num = rand();  // num to compare in consecutive ranges.
chrono::time_point<chrono::system_clock> start, end;
auto start = chrono::system_clock::now();

int inBetween1{ 0 };
for (int i = 1; i < MaxNum; ++i)
{
    if (randVec[i - 1] <= num && num <= randVec[i])
        ++inBetween1;
}
auto end = chrono::system_clock::now();
chrono::duration<double> elapsed_s1 = end - start;

so với câu trả lời được chấp nhận ở trên:

int inBetween2{ 0 };
for (int i = 1; i < MaxNum; ++i)
{
    if (static_cast<unsigned>(num - randVec[i - 1]) <= (randVec[i] - randVec[i - 1]))
        ++inBetween2;
}

Hãy chú ý rằng randVec là một vector được sắp xếp. Đối với mọi kích thước của MaxNum, phương thức đầu tiên sẽ đánh bại phương thức thứ hai trên máy của tôi!


1
Dữ liệu của tôi không được sắp xếp và các thử nghiệm của tôi nằm trên CPU iPhone arm. Kết quả của bạn với dữ liệu và CPU khác nhau có thể khác nhau.
jjxtra

được sắp xếp trong thử nghiệm của tôi chỉ để đảm bảo giới hạn trên không nhỏ hơn giới hạn dưới.
rezeli

1
Các số được sắp xếp có nghĩa là dự đoán nhánh sẽ rất đáng tin cậy và có được tất cả các nhánh ngay ngoại trừ một số ít tại các điểm chuyển đổi. Ưu điểm của mã không phân nhánh là nó sẽ loại bỏ các loại sai sót này trên dữ liệu không thể đoán trước.
Andreas Klebinger

0

Đối với bất kỳ kiểm tra phạm vi biến:

if (x >= minx && x <= maxx) ...

Nó nhanh hơn để sử dụng hoạt động bit:

if ( ((x - minx) | (maxx - x)) >= 0) ...

Điều này sẽ giảm hai nhánh thành một.

Nếu bạn quan tâm về loại an toàn:

if ((int32_t)(((uint32_t)x - (uint32_t)minx) | ((uint32_t)maxx - (uint32_t)x)) > = 0) ...

Bạn có thể kết hợp nhiều phạm vi kiểm tra biến với nhau:

if (( (x - minx) | (maxx - x) | (y - miny) | (maxy - y) ) >= 0) ...

Điều này sẽ giảm 4 nhánh thành 1.

nhanh hơn 3,4 lần so với cái cũ trong gcc:

nhập mô tả hình ảnh ở đây


-4

Có phải là không thể thực hiện một hoạt động bitwise trên số nguyên?

Vì nó phải nằm trong khoảng từ 0 đến 128, nếu bit thứ 8 được đặt (2 ^ 7) thì nó là 128 trở lên. Trường hợp cạnh sẽ là một nỗi đau, mặc dù, vì bạn muốn một so sánh bao gồm.


3
Anh muốn biết nếu x <= end, ở đâu end <= 128. Không phải x <= 128.
Ben Voigt

1
Câu lệnh này " Vì nó phải nằm trong khoảng từ 0 đến 128, nếu bit thứ 8 được đặt (2 ^ 7) thì nó là 128 trở lên " là sai. Hãy xem xét 256.
Happy Green Kid Naps

1
Vâng, rõ ràng tôi đã không nghĩ rằng đủ. Lấy làm tiếc.
icedwater
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.