GNU/Linux >> Belajar Linux >  >> Linux

Bagaimana cara kerja makro yang mungkin/tidak mungkin dalam kernel Linux dan apa manfaatnya?

Mereka memberi petunjuk kepada kompiler untuk mengeluarkan instruksi yang akan menyebabkan prediksi cabang mendukung sisi "kemungkinan" dari instruksi lompat. Ini bisa menjadi kemenangan besar, jika prediksi benar itu berarti instruksi lompat pada dasarnya gratis dan akan memakan waktu nol siklus. Di sisi lain, jika prediksi salah, berarti pipa prosesor perlu dibilas dan dapat menghabiskan beberapa siklus. Selama sebagian besar waktu prediksi benar, ini akan cenderung bagus untuk performa.

Seperti semua pengoptimalan kinerja semacam itu, Anda hanya boleh melakukannya setelah pembuatan profil ekstensif untuk memastikan kode benar-benar berada dalam hambatan, dan mungkin mengingat sifat mikronya, bahwa kode dijalankan dalam putaran yang ketat. Umumnya pengembang Linux cukup berpengalaman jadi saya membayangkan mereka akan melakukannya. Mereka tidak terlalu peduli tentang portabilitas karena mereka hanya menargetkan gcc, dan mereka memiliki gagasan yang sangat dekat tentang rakitan yang ingin mereka hasilkan.


Mari kita dekompilasi untuk melihat apa yang dilakukan GCC 4.8 dengannya

Tanpa __builtin_expect

#include "stdio.h"
#include "time.h"

int main() {
    /* Use time to prevent it from being optimized away. */
    int i = !time(NULL);
    if (i)
        printf("%d\n", i);
    puts("a");
    return 0;
}

Kompilasi dan dekompilasi dengan GCC 4.8.2 x86_64 Linux:

gcc -c -O3 -std=gnu11 main.c
objdump -dr main.o

Keluaran:

0000000000000000 <main>:
   0:       48 83 ec 08             sub    $0x8,%rsp
   4:       31 ff                   xor    %edi,%edi
   6:       e8 00 00 00 00          callq  b <main+0xb>
                    7: R_X86_64_PC32        time-0x4
   b:       48 85 c0                test   %rax,%rax
   e:       75 14                   jne    24 <main+0x24>
  10:       ba 01 00 00 00          mov    $0x1,%edx
  15:       be 00 00 00 00          mov    $0x0,%esi
                    16: R_X86_64_32 .rodata.str1.1
  1a:       bf 01 00 00 00          mov    $0x1,%edi
  1f:       e8 00 00 00 00          callq  24 <main+0x24>
                    20: R_X86_64_PC32       __printf_chk-0x4
  24:       bf 00 00 00 00          mov    $0x0,%edi
                    25: R_X86_64_32 .rodata.str1.1+0x4
  29:       e8 00 00 00 00          callq  2e <main+0x2e>
                    2a: R_X86_64_PC32       puts-0x4
  2e:       31 c0                   xor    %eax,%eax
  30:       48 83 c4 08             add    $0x8,%rsp
  34:       c3                      retq

Urutan instruksi dalam memori tidak berubah:pertama printf lalu puts dan retq kembali.

Dengan __builtin_expect

Sekarang ganti if (i) dengan:

if (__builtin_expect(i, 0))

dan kami mendapatkan:

0000000000000000 <main>:
   0:       48 83 ec 08             sub    $0x8,%rsp
   4:       31 ff                   xor    %edi,%edi
   6:       e8 00 00 00 00          callq  b <main+0xb>
                    7: R_X86_64_PC32        time-0x4
   b:       48 85 c0                test   %rax,%rax
   e:       74 11                   je     21 <main+0x21>
  10:       bf 00 00 00 00          mov    $0x0,%edi
                    11: R_X86_64_32 .rodata.str1.1+0x4
  15:       e8 00 00 00 00          callq  1a <main+0x1a>
                    16: R_X86_64_PC32       puts-0x4
  1a:       31 c0                   xor    %eax,%eax
  1c:       48 83 c4 08             add    $0x8,%rsp
  20:       c3                      retq
  21:       ba 01 00 00 00          mov    $0x1,%edx
  26:       be 00 00 00 00          mov    $0x0,%esi
                    27: R_X86_64_32 .rodata.str1.1
  2b:       bf 01 00 00 00          mov    $0x1,%edi
  30:       e8 00 00 00 00          callq  35 <main+0x35>
                    31: R_X86_64_PC32       __printf_chk-0x4
  35:       eb d9                   jmp    10 <main+0x10>

printf (dikompilasi ke __printf_chk ) dipindahkan ke akhir fungsi, setelah puts dan pengembalian untuk meningkatkan prediksi cabang seperti yang disebutkan oleh jawaban lain.

Jadi pada dasarnya sama dengan:

int main() {
    int i = !time(NULL);
    if (i)
        goto printf;
puts:
    puts("a");
    return 0;
printf:
    printf("%d\n", i);
    goto puts;
}

Pengoptimalan ini tidak dilakukan dengan -O0 .

Tapi semoga berhasil menulis contoh yang berjalan lebih cepat dengan __builtin_expect daripada tanpa, CPU benar-benar pintar akhir-akhir ini. Upaya naif saya ada di sini.

C++20 [[likely]] dan [[unlikely]]

C++20 telah menstandarkan C++ built-in tersebut:Cara menggunakan atribut C++20 kemungkinan/tidak mungkin dalam pernyataan if-else Kemungkinan besar (pun!) akan melakukan hal yang sama.


Ini adalah makro yang memberi petunjuk kepada kompiler tentang ke mana arah cabang. Makro diperluas ke ekstensi khusus GCC, jika tersedia.

GCC menggunakan ini untuk mengoptimalkan prediksi cabang. Misalnya, jika Anda memiliki sesuatu seperti berikut

if (unlikely(x)) {
  dosomething();
}

return x;

Kemudian dapat merestrukturisasi kode ini menjadi sesuatu yang lebih seperti:

if (!x) {
  return x;
}

dosomething();
return x;

Keuntungan dari hal ini adalah ketika prosesor mengambil cabang untuk pertama kalinya, ada overhead yang signifikan, karena mungkin secara spekulatif memuat dan mengeksekusi kode lebih jauh ke depan. Ketika ditentukan akan mengambil cabang, maka harus membatalkannya, dan mulai dari target cabang.

Sebagian besar prosesor modern sekarang memiliki semacam prediksi cabang, tetapi itu hanya membantu jika Anda telah melalui cabang sebelumnya, dan cabang tersebut masih dalam cache prediksi cabang.

Ada sejumlah strategi lain yang dapat digunakan kompiler dan prosesor dalam skenario ini. Anda dapat menemukan detail lebih lanjut tentang cara kerja prediktor cabang di Wikipedia:http://en.wikipedia.org/wiki/Branch_predictor


Linux
  1. Apa itu Perintah Chown di Linux dan Cara Menggunakannya

  2. Apa itu Kernel Linux, dan haruskah Anda mengupgrade ke Kernel terbaru?

  3. Apa perbedaan antara module_init dan init_module dalam modul kernel Linux?

  1. Bagaimana cara men-debug kernel Linux dengan GDB dan QEMU?

  2. Bagaimana dukungan perangkat keras baru ditambahkan ke kernel linux?

  3. Apa perbedaan antara panggilan yang mungkin dan tidak mungkin di Kernel?

  1. Apa itu Rolling Release Linux dan Apa Manfaat Nyata Menggunakannya

  2. Debugging langsung kernel Linux, bagaimana melakukannya dan alat apa yang digunakan?

  3. Bagaimana cara kerja copy_from_user dari kernel Linux secara internal?