Bisakah saya memberi petunjuk pengoptimal dengan memberikan kisaran integer?

173

Saya menggunakan inttipe untuk menyimpan nilai. Dengan semantik program, nilainya selalu bervariasi dalam kisaran yang sangat kecil (0 - 36), dan int(bukan a char) digunakan hanya karena efisiensi CPU.

Sepertinya banyak optimasi aritmatika khusus dapat dilakukan pada sejumlah kecil bilangan bulat. Banyak pemanggilan fungsi pada bilangan bulat itu mungkin dioptimalkan menjadi satu set kecil operasi "ajaib", dan beberapa fungsi bahkan mungkin dioptimalkan ke dalam pencarian tabel.

Jadi, apakah mungkin untuk memberitahu kompiler bahwa ini intselalu dalam kisaran kecil, dan apakah mungkin bagi kompiler untuk melakukan optimasi tersebut?

rolevax
sumber
4
optimisasi rentang nilai ada di banyak kompiler, mis. llvm tapi saya tidak mengetahui petunjuk bahasa apa pun untuk menyatakannya.
Remus Rusanu
2
Perhatikan bahwa jika Anda tidak pernah memiliki angka negatif, Anda mungkin memiliki keuntungan kecil untuk menggunakan unsignedjenis karena mereka lebih mudah untuk dikompilasi dengan kompilator.
user694733
4
@RemusRusanu: Pascal memungkinkan Anda untuk menentukan jenis subrange , misalnya var value: 0..36;.
Edgar Bonet
7
" int (bukan char) digunakan hanya karena efisiensi CPU. " Ini potongan lama kebijaksanaan konvensional biasanya tidak terlalu benar. Jenis-jenis yang sempit kadang-kadang perlu nol atau diperpanjang tanda untuk lebar register penuh, esp. ketika digunakan sebagai indeks array, tetapi kadang-kadang ini terjadi secara gratis. Jika Anda memiliki larik jenis ini, pengurangan jejak cache biasanya melebihi hal lainnya.
Peter Cordes
1
Lupa mengatakan: intdan unsigned intperlu sign-atau zero-extended dari 32 ke 64-bit, juga, pada kebanyakan sistem dengan pointer 64-bit. Perhatikan bahwa pada x86-64, operasi pada register 32-bit nol-rentangkan ke 64-bit gratis (bukan perpanjangan tanda, tetapi overflow yang ditandatangani adalah perilaku yang tidak terdefinisi, sehingga kompiler dapat menggunakan matematika bertanda-tangan 64-bit jika diinginkan). Jadi, Anda hanya melihat instruksi tambahan untuk memperpanjang argumen fungsi 32-bit, bukan hasil perhitungan. Anda akan melakukannya untuk tipe unsigned yang lebih sempit.
Peter Cordes

Jawaban:

230

Ya itu mungkin. Misalnya, untuk gccAnda dapat menggunakan __builtin_unreachableuntuk memberi tahu kompiler tentang kondisi yang tidak mungkin, seperti:

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

Kami dapat membungkus kondisi di atas dalam makro:

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

Dan gunakan seperti ini:

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

Seperti yang Anda lihat , gcclakukan optimasi berdasarkan informasi ini:

#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;
    }
}

Menghasilkan:

func(int):
    mov     eax, 17
    ret

Namun satu kelemahannya adalah bahwa jika kode Anda pernah melanggar asumsi seperti itu, Anda mendapatkan perilaku yang tidak jelas .

Itu tidak memberi tahu Anda ketika ini terjadi, bahkan di build debug. Untuk men-debug / menguji / menangkap bug dengan asumsi lebih mudah, Anda dapat menggunakan makro asumsi / klaim hybrid (kredit ke @ David Z), seperti ini:

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

Dalam membangun debug (dengan NDEBUG tidak didefinisikan), ia bekerja seperti biasa assert, mencetak pesan kesalahan dan abortprogram, dan dalam rilis build itu menggunakan asumsi, menghasilkan kode yang dioptimalkan.

Perhatikan, bagaimanapun, bahwa itu bukan pengganti biasa assert- condtetap dalam rilis rilis, jadi Anda tidak harus melakukan sesuatu seperti assume(VeryExpensiveComputation()).

Peter Mortensen
sumber
5
@ Xofo, tidak mengerti, dalam contoh saya ini sudah terjadi, karena return 2cabang dihilangkan dari kode oleh kompiler.
6
Namun, tampaknya gcc tidak dapat mengoptimalkan fungsi untuk operasi ajaib atau pencarian tabel seperti yang diharapkan OP.
jingyu9575
19
@ user3528438, __builtin_expectadalah petunjuk yang tidak ketat. __builtin_expect(e, c)harus dibaca sebagai " epaling mungkin untuk dievaluasi c", dan dapat bermanfaat untuk mengoptimalkan prediksi cabang, tetapi tidak terbatas euntuk selalu demikian c, jadi jangan biarkan pengoptimal untuk membuang kasus lain. Lihatlah bagaimana cabang-cabang diorganisasi dalam pertemuan .
6
Secara teori, kode apa pun yang tanpa syarat menyebabkan perilaku tidak terdefinisi dapat digunakan sebagai pengganti __builtin_unreachable().
CodesInChaos
14
Kecuali ada beberapa permainan kata-kata saya tidak tahu tentang yang membuat ini ide yang buruk, mungkin masuk akal untuk menggabungkan ini dengan assert, misalnya mendefinisikan assumesebagai assertketika NDEBUGtidak didefinisikan, dan sebagai __builtin_unreachable()saat NDEBUGdidefinisikan. Dengan begitu Anda mendapatkan manfaat dari asumsi dalam kode produksi, tetapi dalam membangun debug Anda masih memiliki pemeriksaan eksplisit. Tentu saja Anda harus melakukan tes yang cukup untuk meyakinkan diri sendiri bahwa anggapan tersebut akan memuaskan di alam liar.
David Z
61

Ada dukungan standar untuk ini. Apa yang harus Anda lakukan adalah memasukkan stdint.h( cstdint) dan kemudian menggunakan jenisnya uint_fast8_t.

Ini memberitahu kompiler bahwa Anda hanya menggunakan angka antara 0 - 255, tetapi itu bebas untuk menggunakan tipe yang lebih besar jika itu memberikan kode lebih cepat. Demikian pula, kompiler dapat mengasumsikan bahwa variabel tidak akan pernah memiliki nilai di atas 255 dan kemudian melakukan optimasi yang sesuai.

Lundin
sumber
2
Jenis-jenis ini tidak digunakan sebanyak yang seharusnya (saya pribadi cenderung lupa bahwa mereka ada). Mereka memberikan kode yang cepat dan portabel, cukup brilian. Dan mereka sudah ada sejak 1999.
Lundin
Ini adalah saran yang bagus untuk kasus umum. Jawaban deniss menunjukkan solusi yang lebih lunak untuk skenario tertentu.
Lightness Races in Orbit
1
Kompiler hanya mendapatkan info rentang 0-255 pada sistem uint_fast8_tyang sebenarnya merupakan tipe 8-bit (mis. unsigned char) Seperti pada x86 / ARM / MIPS / PPC ( godbolt.org/g/KNyc31 ). Pada DEC Alpha awal sebelum 21164A , byte byte / toko tidak didukung, sehingga implementasi yang waras akan digunakan typedef uint32_t uint_fast8_t. AFAIK, tidak ada mekanisme untuk tipe untuk memiliki batas jangkauan ekstra dengan kebanyakan kompiler (seperti gcc), jadi saya cukup yakin uint_fast8_takan berperilaku sama persis dengan unsigned intatau apa pun dalam kasus itu.
Peter Cordes
( boolkhusus, dan rentang-terbatas pada 0 atau 1, tetapi ini adalah tipe bawaan, tidak ditentukan oleh file header dalam hal char, pada gcc / dentang. Seperti yang saya katakan, saya tidak berpikir kebanyakan kompiler memiliki mekanisme itu akan memungkinkan.)
Peter Cordes
1
Lagi pula, uint_fast8_tini adalah rekomendasi yang bagus, karena akan menggunakan tipe 8-bit pada platform di mana itu seefisien unsigned int. (Aku sebenarnya tidak yakin apa fastjenis seharusnya cepat untuk , dan apakah cache jejak tradeoff seharusnya menjadi bagian dari itu.). x86 memiliki dukungan luas untuk operasi byte, bahkan untuk melakukan byte tambah dengan sumber memori, sehingga Anda bahkan tidak perlu melakukan beban zero-extending terpisah (yang juga sangat murah). gcc membuat uint_fast16_ttipe 64-bit pada x86, yang gila untuk sebagian besar kegunaan (vs 32-bit). godbolt.org/g/Rmq5bv .
Peter Cordes
8

Jawaban saat ini baik untuk kasus saat Anda tahu pasti kisarannya, tetapi jika Anda masih menginginkan perilaku yang benar saat nilainya di luar kisaran yang diharapkan, maka tidak akan berfungsi.

Untuk itu, saya menemukan teknik ini dapat bekerja:

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

Idenya adalah pertukaran kode-data: Anda memindahkan 1 bit data (apakah x == c) ke dalam logika kontrol .
Ini mengisyaratkan ke pengoptimal yang xsebenarnya adalah konstanta yang diketahui c, mendorongnya untuk menyesuaikan dan mengoptimalkan permintaan pertama foosecara terpisah dari yang lain, mungkin cukup berat.

Pastikan untuk benar-benar memasukkan kode ke dalam satu subrutin foo, - jangan menduplikasi kodenya.

Contoh:

Agar teknik ini berfungsi, Anda harus sedikit beruntung - ada kasus di mana kompiler memutuskan untuk tidak mengevaluasi hal-hal secara statis, dan mereka agak sewenang-wenang. Tetapi ketika berhasil, itu bekerja dengan baik:

#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);
}

Cukup gunakan -O3dan perhatikan konstanta pra-evaluasi 0x20dan 0x30edalam output assembler .

pengguna541686
sumber
Apakah kamu tidak mau if (x==c) foo(c) else foo(x)? Jika hanya untuk menangkap constexprimplementasi foo?
MSalters
@Malters: Saya tahu seseorang akan menanyakan itu !! Saya datang dengan teknik ini sebelumnya constexpradalah suatu hal dan tidak pernah repot-repot untuk "memperbaruinya" setelah itu (meskipun saya tidak pernah benar-benar peduli untuk mengkhawatirkan constexprbahkan sesudahnya), tetapi alasan saya tidak melakukannya pada awalnya adalah bahwa saya ingin membuatnya lebih mudah bagi kompiler untuk memfaktorkannya sebagai kode umum dan menghapus cabang jika memutuskan untuk membiarkan mereka sebagai pemanggilan metode normal dan tidak mengoptimalkan. Saya berharap jika saya memasukkannya csangat sulit bagi kompiler untuk c (maaf, lelucon buruk) bahwa keduanya adalah kode yang sama, meskipun saya tidak pernah memverifikasi ini.
user541686
4

Saya hanya ingin mengatakan bahwa jika Anda ingin solusi yang lebih standar C ++, Anda dapat menggunakan [[noreturn]]atribut untuk menulis sendiriunreachable .

Jadi saya akan mengarahkan kembali contoh bagus deniss ' untuk menunjukkan:

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;
    }
}

Yang seperti yang Anda lihat , menghasilkan kode yang hampir identik:

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

Kelemahannya tentu saja, bahwa Anda mendapatkan peringatan bahwa suatu [[noreturn]]fungsi memang kembali.

StoryTeller - Unslander Monica
sumber
Ini bekerja dengan clang, ketika solusi asli saya tidak , trik yang bagus dan +1. Tetapi semuanya sangat bergantung pada kompiler (seperti yang ditunjukkan oleh Peter Cordes kepada kami, icchal itu dapat memperburuk kinerja), sehingga masih belum dapat diterapkan secara universal. Juga, catatan kecil: unreachabledefinisi harus tersedia untuk pengoptimal dan inline agar ini berfungsi .