Tôi có thể gợi ý trình tối ưu hóa bằng cách đưa ra phạm vi của một số nguyên không?


173

Tôi đang sử dụng một intloại để lưu trữ một giá trị. Theo ngữ nghĩa của chương trình, giá trị luôn thay đổi trong một phạm vi rất nhỏ (0 - 36) và int(không phải a char) chỉ được sử dụng vì hiệu quả của CPU.

Có vẻ như nhiều tối ưu hóa số học đặc biệt có thể được thực hiện trên một phạm vi số nguyên nhỏ như vậy. Nhiều lệnh gọi hàm trên các số nguyên đó có thể được tối ưu hóa thành một tập hợp nhỏ các phép toán "ma thuật" và một số hàm thậm chí có thể được tối ưu hóa để tra cứu bảng.

Vì vậy, có thể nói với trình biên dịch rằng điều này intluôn nằm trong phạm vi nhỏ đó và liệu trình biên dịch có thể thực hiện những tối ưu hóa đó không?


4
tối ưu hóa phạm vi giá trị tồn tại trong nhiều trình biên dịch, ví dụ. llvm nhưng tôi không biết bất kỳ gợi ý ngôn ngữ nào để khai báo nó.
Remus Rusanu

2
Lưu ý rằng nếu bạn chưa bao giờ có số âm, bạn có thể có lợi ích nhỏ khi sử dụng unsignedcác loại vì chúng dễ dàng hơn cho trình biên dịch.
dùng694733

4
@RemusRusanu: Pascal cho phép bạn xác định các loại phụ , vd var value: 0..36;.
Edgar Bonet

7
" Int (không phải char) chỉ được sử dụng vì hiệu quả của CPU. " Phần khôn ngoan thông thường cũ này thường không đúng lắm. Các loại hẹp đôi khi cần phải bằng 0 hoặc mở rộng ký hiệu cho chiều rộng thanh ghi đầy đủ, đặc biệt. khi được sử dụng như các chỉ số mảng, nhưng đôi khi điều này xảy ra miễn phí. Nếu bạn có một mảng thuộc loại này, việc giảm dấu chân bộ đệm thường vượt trội hơn bất cứ thứ gì khác.
Peter Cordes

1
Quên không nói: intvà cũng unsigned intcần được ký hiệu hoặc mở rộng từ 32 đến 64 bit, trên hầu hết các hệ thống có con trỏ 64 bit. Lưu ý rằng trên x86-64, các thao tác trên các thanh ghi 32 bit không mở rộng thành 64 bit miễn phí (không phải ký hiệu mở rộng, nhưng tràn ký hiệu là hành vi không xác định, do đó trình biên dịch chỉ có thể sử dụng toán học có chữ ký 64 bit nếu muốn). Vì vậy, bạn chỉ thấy các hướng dẫn bổ sung cho hàm 32 bit không mở rộng, không phải là kết quả tính toán. Bạn sẽ cho các loại không dấu hẹp hơn.
Peter Cordes

Câu trả lời:


230

Vâng, nó là có thể. Ví dụ: đối với gccbạn có thể sử dụng __builtin_unreachableđể báo cho trình biên dịch về các điều kiện không thể, như vậy:

if (value < 0 || value > 36) __builtin_unreachable();

Chúng ta có thể gói các điều kiện trên trong một macro:

#define assume(cond) do { if (!(cond)) __builtin_unreachable(); } while (0)

Và sử dụng nó như vậy:

assume(x >= 0 && x <= 10);

Như bạn có thể thấy , gccthực hiện tối ưu hóa dựa trên thông tin này:

#define assume(cond) do { if (!(cond)) __builtin_unreachable(); } while (0)

int func(int x){
    assume(x >=0 && x <= 10);

    if (x > 11){
        return 2;
    }
    else{
        return 17;
    }
}

Sản xuất:

func(int):
    mov     eax, 17
    ret

Tuy nhiên, một nhược điểm là nếu mã của bạn từng phá vỡ các giả định như vậy, bạn sẽ có hành vi không xác định .

Nó không thông báo cho bạn khi điều này xảy ra, ngay cả trong các bản dựng gỡ lỗi. Để gỡ lỗi / kiểm tra / bắt lỗi với các giả định dễ dàng hơn, bạn có thể sử dụng macro giả định / xác nhận kết hợp (tín dụng cho @David Z), như thế này:

#if defined(NDEBUG)
#define assume(cond) do { if (!(cond)) __builtin_unreachable(); } while (0)
#else
#include <cassert>
#define assume(cond) assert(cond)
#endif

Trong các bản dựng gỡ lỗi ( NDEBUG không được xác định), nó hoạt động như một chương trình thông assertbáo lỗi và in thông thường abort, và trong bản phát hành, nó sử dụng một giả định, tạo ra mã được tối ưu hóa.

Tuy nhiên, lưu ý rằng nó không thay thế cho thường xuyên assert- condvẫn còn trong các bản dựng phát hành, vì vậy bạn không nên làm điều gì đó như thế assume(VeryExpensiveComputation()).


5
@Xofo, đã không nhận được nó, trong ví dụ của tôi điều này đã xảy ra, khi return 2chi nhánh bị loại khỏi mã bởi trình biên dịch.

6
Tuy nhiên, dường như gcc không thể tối ưu hóa các chức năng thành các hoạt động ma thuật hoặc tra cứu bảng như OP mong đợi.
jingyu9575

19
@ user3528438, __builtin_expectlà một gợi ý không nghiêm ngặt. __builtin_expect(e, c)nên đọc là " erất có thể đánh giá c" và có thể hữu ích để tối ưu hóa dự đoán chi nhánh, nhưng nó không hạn chế eluôn luôn c, vì vậy không cho phép trình tối ưu hóa loại bỏ các trường hợp khác. Nhìn cách các chi nhánh được tổ chức trong lắp ráp .

6
Về lý thuyết, bất kỳ mã nào gây ra vô điều kiện hành vi không xác định có thể được sử dụng thay thế __builtin_unreachable().
CodeInChaos

14
Trừ khi có một vài điều khó hiểu mà tôi không biết về điều đó làm cho điều này trở thành một ý tưởng tồi, thì có thể có ý nghĩa khi kết hợp điều này với assert, ví dụ như xác địnhassume như assertkhi NDEBUGkhông được xác định, và như __builtin_unreachable()khi NDEBUGđược xác định. Bằng cách đó bạn có được lợi ích của giả định trong mã sản xuất, nhưng trong bản dựng gỡ lỗi, bạn vẫn có một kiểm tra rõ ràng. Tất nhiên sau đó bạn phải làm đủ các bài kiểm tra để đảm bảo với bản thân rằng giả định sẽ được thỏa mãn trong tự nhiên.
David Z

61

Có hỗ trợ tiêu chuẩn cho việc này. Những gì bạn nên làm là bao gồm stdint.h( cstdint) và sau đó sử dụng loại uint_fast8_t.

Điều này cho trình biên dịch biết rằng bạn chỉ sử dụng các số trong khoảng 0 - 255, nhưng việc sử dụng loại lớn hơn là miễn phí nếu điều đó cho mã nhanh hơn. Tương tự, trình biên dịch có thể giả định rằng biến sẽ không bao giờ có giá trị trên 255 và sau đó thực hiện tối ưu hóa tương ứng.


2
Những loại này không được sử dụng nhiều như chúng nên (cá nhân tôi có xu hướng quên rằng chúng tồn tại). Họ cung cấp mã nhanh và di động, khá tuyệt vời. Và chúng đã tồn tại từ năm 1999.
Lundin

Đây là một gợi ý tốt cho trường hợp chung. câu trả lời của deniss cho thấy một giải pháp dễ uốn hơn cho các tình huống cụ thể.
Các cuộc đua nhẹ nhàng trong quỹ đạo

1
Trình biên dịch chỉ nhận được thông tin phạm vi 0-255 trên các hệ thống uint_fast8_tthực sự là loại 8 bit (ví dụ unsigned char) giống như trên x86 / ARM / MIPS / PPC ( godbolt.org/g/KNyc31 ). Vào đầu DEC Alpha trước 21164A , tải / lưu trữ byte không được hỗ trợ, do đó, bất kỳ triển khai lành mạnh nào cũng sẽ sử dụng typedef uint32_t uint_fast8_t. AFAIK, không có cơ chế cho một loại có thêm giới hạn phạm vi với hầu hết các trình biên dịch (như gcc), vì vậy tôi khá chắc chắn uint_fast8_tsẽ hành xử giống hệt như unsigned inthoặc bất cứ điều gì trong trường hợp đó.
Peter Cordes

( boolđặc biệt và bị giới hạn phạm vi là 0 hoặc 1, nhưng đó là loại tích hợp, không được xác định bởi các tệp tiêu đề về char, trên gcc / clang. Như tôi đã nói, tôi không nghĩ rằng hầu hết các trình biên dịch đều có cơ chế điều đó sẽ biến điều đó thành có thể.)
Peter Cordes

1
Dù sao, uint_fast8_tlà một đề xuất tốt, vì nó sẽ sử dụng loại 8 bit trên các nền tảng có hiệu quả như unsigned int. (Tôi thực sự không chắc chắn về fastloại có nghĩa vụ phải được nhanh chóng cho , và liệu bộ nhớ cache dấu chân cân bằng được coi là một phần của nó.). x86 có hỗ trợ rộng rãi cho các hoạt động byte, ngay cả khi thực hiện thêm byte bằng nguồn bộ nhớ, do đó bạn thậm chí không phải thực hiện tải mở rộng bằng 0 riêng biệt (cũng rất rẻ). gcc tạo ra uint_fast16_tloại 64 bit trên x86, đây là loại điên cho hầu hết các mục đích sử dụng (so với 32 bit). godbolt.org/g/Rmq5bv .
Peter Cordes

8

Câu trả lời hiện tại phù hợp với trường hợp khi bạn biết chắc chắn phạm vi đó là gì, nhưng nếu bạn vẫn muốn hành vi đúng khi giá trị nằm ngoài phạm vi dự kiến, thì nó sẽ không hoạt động.

Trong trường hợp đó, tôi thấy kỹ thuật này có thể hoạt động:

if (x == c)  // assume c is a constant
{
    foo(x);
}
else
{
    foo(x);
}

Ý tưởng là một sự đánh đổi dữ liệu mã: bạn đang chuyển 1 bit dữ liệu (cho dù x == c) vào logic điều khiển .
Điều này gợi ý cho trình tối ưu hóa xtrên thực tế là một hằng số đã biết c, khuyến khích nó thực hiện nội tuyến và tối ưu hóa lệnh gọi đầu tiên footách biệt với phần còn lại, có thể khá nặng nề.

fooMặc dù vậy, hãy đảm bảo thực sự đặt mã vào một chương trình con duy nhất - không sao chép mã.

Thí dụ:

Để kỹ thuật này hoạt động, bạn cần phải có một chút may mắn - có những trường hợp trình biên dịch quyết định không đánh giá mọi thứ một cách tĩnh và chúng là loại tùy ý. Nhưng khi nó hoạt động, nó hoạt động tốt:

#include <math.h>
#include <stdio.h>

unsigned foo(unsigned x)
{
    return x * (x + 1);
}

unsigned bar(unsigned x) { return foo(x + 1) + foo(2 * x); }

int main()
{
    unsigned x;
    scanf("%u", &x);
    unsigned r;
    if (x == 1)
    {
        r = bar(bar(x));
    }
    else if (x == 0)
    {
        r = bar(bar(x));
    }
    else
    {
        r = bar(x + 1);
    }
    printf("%#x\n", r);
}

Chỉ cần sử dụng -O3và chú ý các hằng số được đánh giá trước 0x200x30etrong đầu ra của trình biên dịch chương trình .


Bạn có muốn if (x==c) foo(c) else foo(x)không? Nếu chỉ để bắt constexprthực hiện foo?
MSalters

@MSalters: Tôi biết ai đó sẽ hỏi điều đó !! Tôi đã nghĩ ra kỹ thuật này trước đây constexprlà một điều và không bao giờ bận tâm đến việc "cập nhật" nó sau đó (mặc dù tôi thực sự không bao giờ bận tâm về việc constexprthậm chí sau đó), nhưng lý do ban đầu tôi không làm điều đó là vì tôi muốn làm cho trình biên dịch dễ dàng biến chúng thành mã chung và loại bỏ nhánh nếu nó quyết định để chúng như các cuộc gọi phương thức bình thường và không tối ưu hóa. Tôi dự đoán nếu tôi đưa vào ctrình biên dịch thực sự khó khăn cho c (xin lỗi, dở khóc dở cười) rằng hai cái đó là cùng một mã, mặc dù tôi chưa bao giờ xác minh điều này.
dùng541686

4

Tôi chỉ nói rằng nếu bạn có thể muốn một giải pháp C ++ chuẩn hơn, bạn có thể sử dụng [[noreturn]]thuộc tính để viết riêng của bạnunreachable .

Vì vậy, tôi sẽ tái sử dụng ví dụ tuyệt vời của deniss để chứng minh:

namespace detail {
    [[noreturn]] void unreachable(){}
}

#define assume(cond) do { if (!(cond)) detail::unreachable(); } while (0)

int func(int x){
    assume(x >=0 && x <= 10);

    if (x > 11){
        return 2;
    }
    else{
        return 17;
    }
}

như bạn có thể thấy , kết quả trong mã gần giống hệt nhau:

detail::unreachable():
        rep ret
func(int):
        movl    $17, %eax
        ret

Nhược điểm là tất nhiên, rằng bạn nhận được một cảnh báo rằng một [[noreturn]]chức năng thực sự trở lại.


Nó hoạt động với clang, khi giải pháp ban đầu của tôi không có , thủ thuật hay và +1. Nhưng toàn bộ điều này phụ thuộc rất nhiều vào trình biên dịch (như Peter Cordes đã cho chúng ta thấy, trong iccđó có thể làm giảm hiệu suất), vì vậy nó vẫn không được áp dụng phổ biến. Ngoài ra, lưu ý nhỏ: unreachableđịnh nghĩa phải có sẵn để tối ưu hóa và nội tuyến để làm việc này .
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.