Apa pendekatan terbaik ketika menulis fungsi untuk perangkat lunak tertanam untuk mendapatkan kinerja yang lebih baik? [Tutup]


13

Saya telah melihat beberapa perpustakaan untuk mikrokontroler dan dan fungsinya melakukan satu hal pada satu waktu. Misalnya, sesuatu seperti ini:

void setCLK()
{
    // Code to set the clock
}

void setConfig()
{
    // Code to set the config
}

void setSomethingElse()
{
   // 1 line code to write something to a register.
}

Kemudian datang fungsi lain di atasnya yang menggunakan kode 1 baris ini yang berisi fungsi untuk melayani tujuan lain. Sebagai contoh:

void initModule()
{
   setCLK();
   setConfig();
   setSomethingElse();
}

Saya tidak yakin, tapi saya percaya dengan cara ini akan membuat lebih banyak panggilan untuk melompat dan menciptakan overhead untuk menumpuk alamat kembali setiap kali fungsi dipanggil atau keluar. Dan itu akan membuat program bekerja lambat, bukan?

Saya telah mencari dan di mana-mana mereka mengatakan bahwa aturan praktis pemrograman adalah bahwa suatu fungsi seharusnya hanya melakukan satu tugas.

Jadi jika saya menulis langsung modul fungsi InitModule yang mengatur jam, menambahkan beberapa konfigurasi yang diinginkan dan melakukan sesuatu yang lain tanpa memanggil fungsi. Apakah ini pendekatan yang buruk ketika menulis perangkat lunak tertanam?


EDIT 2:

  1. Sepertinya banyak orang telah memahami pertanyaan ini seolah-olah saya mencoba untuk mengoptimalkan suatu program. Tidak, saya tidak punya niat untuk melakukannya . Saya membiarkan kompiler melakukannya, karena itu akan selalu (saya harap tidak sekalipun!) Lebih baik daripada saya.

  2. Semua menyalahkan saya untuk memilih contoh yang mewakili beberapa kode inisialisasi . Pertanyaannya tidak memiliki niat tentang pemanggilan fungsi yang dilakukan untuk tujuan inisialisasi. Pertanyaan saya adalah Apakah memecah tugas tertentu menjadi fungsi-fungsi kecil multi-line ( jadi in-line keluar dari pertanyaan ) berjalan di dalam infinite loop memiliki keunggulan dibandingkan menulis fungsi panjang tanpa fungsi bersarang?

Silakan mempertimbangkan keterbacaan yang ditentukan dalam jawaban @ Jonk .


28
Anda sangat naif (tidak dimaksudkan sebagai penghinaan) jika Anda percaya bahwa kompiler yang masuk akal akan secara membuta mengubah kode yang ditulis menjadi binari seperti yang ditulis. Sebagian besar kompiler modern cukup baik dalam mengidentifikasi kapan rutin lebih baik digarisbawahi dan bahkan ketika lokasi register vs RAM harus digunakan untuk memegang variabel. Ikuti dua aturan optimasi: 1) jangan optimalkan. 2) tidak mengoptimalkan belum . Jadikan kode Anda mudah dibaca dan dipelihara, dan LALU hanya setelah membuat profil sistem yang berfungsi, lihat untuk mengoptimalkan.
akohlsmith

10
@akohlsmith IIRC Tiga aturan optimasi adalah: 1) Jangan! 2) Tidak benar-benar Jangan! 3) Profil pertama, kemudian dan hanya kemudian mengoptimalkan jika Anda harus - Michael_A._Jackson
esoterik

3
Ingatlah bahwa "optimasi prematur adalah akar dari semua kejahatan (atau setidaknya sebagian besar) dalam pemrograman" - Knuth
Mawg mengatakan mengembalikan Monica

1
@Mawg: Kata operatif di sana terlalu dini . (Seperti paragraf berikutnya dalam makalah itu menjelaskan. Secara harfiah kalimat berikutnya: "Namun kita tidak boleh melewatkan peluang kita dalam 3% kritis itu.") Jangan mengoptimalkan sampai Anda perlu - Anda tidak akan menemukan yang lambat bit sampai Anda memiliki sesuatu untuk profil - tetapi juga tidak terlibat dalam pesimisasi, misalnya dengan menggunakan alat yang salah untuk pekerjaan itu.
cHao

1
@ Mawg Saya tidak tahu mengapa saya mendapat jawaban / umpan balik terkait dengan pengoptimalan, karena saya tidak pernah menyebut kata dan saya bermaksud melakukannya. Pertanyaannya lebih tentang bagaimana menulis fungsi dalam Pemrograman Tertanam untuk mencapai kinerja yang lebih baik.
MaNyYaCk

Jawaban:


28

Bisa dibilang, dalam contoh Anda kinerja tidak akan masalah, karena kode hanya dijalankan sekali saat startup.

Aturan praktis yang saya gunakan: Tulis kode Anda semudah mungkin dibaca dan hanya mulai mengoptimalkan jika Anda melihat bahwa kompiler Anda tidak melakukan sihirnya dengan benar.

Biaya panggilan fungsi dalam ISR mungkin sama dengan biaya panggilan fungsi saat startup dalam hal penyimpanan dan waktu. Namun, persyaratan waktu selama ISR itu mungkin jauh lebih kritis.

Lebih jauh, seperti yang telah diperhatikan oleh orang lain, biaya (dan makna 'biaya') dari panggilan fungsi berbeda menurut platform, kompiler, pengaturan optimisasi kompiler, dan persyaratan aplikasi. Akan ada perbedaan besar antara 8051 dan korteks-m7, dan alat pacu jantung dan lampu.


6
IMO paragraf kedua harus dicetak tebal dan di atas. Tidak ada yang salah dengan memilih algoritma dan struktur data yang tepat, tetapi khawatir tentang fungsi panggilan overhead kecuali Anda telah menemukan bahwa itu adalah hambatan yang sebenarnya pasti optimasi prematur, dan harus dihindari.
Dana Gugatan Monica

11

Tidak ada keuntungan yang dapat saya pikirkan (tetapi lihat catatan untuk JasonS di bagian bawah), yang membungkus satu baris kode sebagai fungsi atau subrutin. Kecuali mungkin Anda bisa memberi nama fungsi itu sesuatu yang "dapat dibaca." Tapi Anda bisa saja berkomentar. Dan karena membungkus satu baris kode dalam suatu fungsi memerlukan kode memori, ruang stack, dan waktu eksekusi, menurut saya sebagian besar kontraproduktif. Dalam situasi mengajar? Mungkin masuk akal. Tetapi itu tergantung pada kelas siswa, persiapan mereka sebelumnya, kurikulum, dan guru. Sebagian besar, saya pikir itu bukan ide yang baik. Tapi itu menurut saya.

Yang membawa kita pada intinya. Area pertanyaan Anda yang luas telah, selama beberapa dekade, menjadi bahan perdebatan dan hingga hari ini masih menjadi bahan perdebatan. Jadi, setidaknya saat saya membaca pertanyaan Anda, bagi saya itu sepertinya pertanyaan berdasarkan pendapat (seperti yang Anda tanyakan).

Itu bisa dipindahkan dari menjadi berbasis opini seperti itu, jika Anda lebih rinci tentang situasi dan dengan hati-hati menggambarkan tujuan yang Anda pegang sebagai yang utama. Semakin baik Anda mendefinisikan alat ukur Anda, semakin objektif jawabannya.


Secara umum, Anda ingin melakukan hal berikut untuk pengkodean apa pun . (Untuk di bawah ini, saya akan berasumsi bahwa kami membandingkan berbagai pendekatan yang semuanya mencapai tujuan. Jelas, setiap kode yang gagal melakukan tugas yang diperlukan lebih buruk daripada kode yang berhasil, terlepas dari bagaimana itu ditulis.)

  1. Konsisten dengan pendekatan Anda, sehingga orang lain yang membaca kode Anda dapat mengembangkan pemahaman tentang bagaimana Anda mendekati proses pengkodean Anda. Menjadi tidak konsisten mungkin merupakan kejahatan terburuk yang mungkin terjadi. Ini tidak hanya mempersulit orang lain, tetapi juga menyulitkan Anda untuk kembali ke kode bertahun-tahun kemudian.
  2. Sedapat mungkin, coba dan atur hal-hal sehingga inisialisasi berbagai bagian fungsional dapat dilakukan tanpa memperhatikan pemesanan. Dimana diperlukan pemesanan, jika itu karena kopling dekat dari dua subfungsi yang sangat terkait, maka pertimbangkan inisialisasi tunggal untuk keduanya sehingga dapat dipesan ulang tanpa menyebabkan kerusakan. Jika itu tidak memungkinkan, maka dokumentasikan persyaratan pemesanan inisialisasi.
  3. Meringkas pengetahuan di satu tempat, jika memungkinkan. Konstanta tidak boleh digandakan di semua tempat dalam kode. Persamaan yang memecahkan beberapa variabel harus ada di satu dan hanya satu tempat. Dan seterusnya. Jika Anda menemukan diri Anda menyalin dan menempel beberapa set garis yang melakukan perilaku yang diperlukan di berbagai lokasi, pertimbangkan cara untuk menangkap pengetahuan itu di satu tempat dan menggunakannya di tempat yang diperlukan. Misalnya, jika Anda memiliki struktur pohon yang harus berjalan dengan cara tertentu, lakukan tidakmereplikasi kode pohon berjalan di setiap tempat di mana Anda perlu untuk loop melalui simpul pohon. Alih-alih, tangkap metode berjalan pohon di satu tempat dan gunakan. Dengan cara ini, jika pohon berubah dan metode berjalan berubah, Anda hanya memiliki satu tempat yang perlu dikhawatirkan dan semua kode lainnya "berfungsi dengan baik."
  4. Jika Anda menyebarkan semua rutinitas Anda ke selembar kertas yang besar dan rata, dengan panah yang menghubungkannya dengan rutinitas lain, Anda akan melihat dalam aplikasi apa pun akan ada "kelompok" rutin yang memiliki banyak dan banyak anak panah antara mereka sendiri tetapi hanya beberapa panah di luar grup. Jadi akan ada batasan alami dari rutinitas yang dipasangkan dengan erat dan koneksi yang dipasangkan secara longgar di antara kelompok-kelompok lain dari rutinitas yang dipasangkan dengan erat. Gunakan fakta ini untuk mengatur kode Anda ke dalam modul. Ini akan membatasi kompleksitas kode Anda yang nyata.

Di atas umumnya hanya benar tentang semua coding. Saya tidak membahas penggunaan parameter, variabel global lokal atau statis, dll. Alasannya adalah bahwa untuk pemrograman tertanam ruang aplikasi sering menempatkan kendala baru yang ekstrim dan sangat signifikan dan tidak mungkin untuk membahas semuanya tanpa membahas setiap aplikasi yang disematkan. Dan itu tidak terjadi di sini.

Kendala-kendala ini dapat berupa (dan lebih banyak) di antaranya:

  • Keterbatasan biaya yang parah membutuhkan MCU yang sangat primitif dengan RAM sangat kecil dan hampir tanpa pin I / O. Untuk ini, seluruh rangkaian aturan baru berlaku. Misalnya, Anda mungkin harus menulis dalam kode assembly karena tidak ada banyak ruang kode. Anda mungkin harus menggunakan variabel statis HANYA karena penggunaan variabel lokal terlalu mahal dan memakan waktu. Anda mungkin harus menghindari penggunaan subrutin yang berlebihan karena (misalnya, beberapa bagian PIC Microchip) hanya ada 4 register perangkat keras untuk menyimpan alamat pengirim subrutin. Jadi, Anda mungkin harus secara dramatis "meratakan" kode Anda. Dll
  • Keterbatasan daya yang parah membutuhkan kode yang dibuat dengan hati-hati untuk memulai dan mematikan sebagian besar MCU dan menempatkan batasan parah pada waktu eksekusi kode ketika berjalan dengan kecepatan penuh. Sekali lagi, ini mungkin memerlukan beberapa kode perakitan, kadang-kadang.
  • Persyaratan waktu yang berat. Sebagai contoh, ada saat-saat di mana saya harus memastikan bahwa transmisi saluran terbuka 0 harus menggunakan jumlah siklus yang sama persis dengan transmisi 1. Dan bahwa pengambilan sampel pada jalur yang sama ini juga harus dilakukan dengan fase relatif yang tepat untuk waktu ini. Ini berarti bahwa C TIDAK dapat digunakan di sini. Cara HANYA yang mungkin untuk membuat jaminan itu adalah dengan hati-hati menyusun kode perakitan. (Dan bahkan kemudian, tidak selalu pada semua desain ALU.)

Dan seterusnya. (Kode pengkabelan untuk instrumentasi medis yang kritis terhadap kehidupan memiliki seluruh dunianya sendiri.)

Hasilnya di sini adalah bahwa pengkodean yang disematkan seringkali bukan program gratis untuk semua, di mana Anda dapat membuat kode seperti yang Anda lakukan di workstation. Seringkali ada alasan yang kuat dan kompetitif untuk berbagai kendala yang sangat sulit. Dan ini mungkin sangat menentang jawaban yang lebih tradisional dan stok .


Mengenai keterbacaan, saya menemukan bahwa kode dapat dibaca jika ditulis dengan cara yang konsisten yang dapat saya pelajari saat saya membacanya. Dan di mana tidak ada upaya yang disengaja untuk mengaburkan kode. Sebenarnya tidak banyak yang diperlukan.

Kode yang dapat dibaca bisa sangat efisien dan dapat memenuhi semua persyaratan di atas yang telah saya sebutkan. Hal utama adalah bahwa Anda sepenuhnya memahami apa yang dihasilkan setiap baris kode yang Anda tulis pada tingkat perakitan atau mesin, saat Anda membuat kode. C ++ menempatkan beban yang serius pada programmer di sini karena ada banyak situasi di mana identik potongan kode C ++ benar-benar menghasilkan berbagai potongan kode mesin yang memiliki kinerja sangat berbeda. Tapi C, umumnya, sebagian besar adalah bahasa "apa yang Anda lihat adalah apa yang Anda dapatkan". Jadi lebih aman dalam hal itu.


EDIT per JasonS:

Saya telah menggunakan C sejak 1978 dan C ++ sejak sekitar 1987 dan saya memiliki banyak pengalaman menggunakan keduanya untuk mainframe, minicomputer, dan (kebanyakan) aplikasi yang disematkan.

Jason memunculkan komentar tentang menggunakan 'inline' sebagai pengubah. (Dalam perspektif saya, ini adalah kemampuan yang relatif "baru" karena itu tidak ada selama mungkin setengah dari hidup saya atau lebih menggunakan C dan C ++.) Penggunaan fungsi inline sebenarnya dapat membuat panggilan seperti itu (bahkan untuk satu baris kode) cukup praktis. Dan itu jauh lebih baik, jika memungkinkan, daripada menggunakan makro karena pengetikan yang dapat diterapkan oleh kompiler.

Tetapi ada keterbatasan juga. Yang pertama adalah Anda tidak bisa mengandalkan kompiler untuk "menerima petunjuk." Mungkin, atau mungkin tidak. Dan ada alasan bagus untuk tidak menerima petunjuk itu. (Untuk contoh yang jelas, jika alamat fungsi diambil, ini membutuhkan instantiasi fungsi dan penggunaan alamat untuk membuat panggilan akan ... memerlukan panggilan. Kode tidak dapat digariskan kemudian.) Ada alasan lain juga. Kompiler dapat memiliki beragam kriteria yang digunakan untuk menilai cara menangani petunjuk. Dan sebagai seorang programmer, ini berarti Anda harusLuangkan waktu untuk mempelajari aspek kompiler itu atau Anda mungkin akan membuat keputusan berdasarkan ide-ide yang salah. Jadi itu menambah beban bagi penulis kode dan juga setiap pembaca dan siapa pun yang berencana untuk port kode ke beberapa kompiler lain, juga.

Juga, kompiler C dan C ++ mendukung kompilasi terpisah. Ini berarti bahwa mereka dapat mengkompilasi satu bagian dari kode C atau C ++ tanpa mengkompilasi kode terkait lainnya untuk proyek tersebut. Untuk kode inline, anggap kompiler jika tidak memilih untuk melakukannya, itu tidak hanya harus memiliki deklarasi "dalam ruang lingkup" tetapi juga harus memiliki definisi, juga. Biasanya, programmer akan bekerja untuk memastikan bahwa ini adalah kasusnya jika mereka menggunakan 'inline'. Tetapi mudah untuk kesalahan untuk masuk.

Secara umum, sementara saya juga menggunakan inline di mana saya pikir itu sesuai, saya cenderung berasumsi bahwa saya tidak dapat mengandalkannya. Jika kinerja adalah persyaratan yang signifikan, dan saya pikir OP telah dengan jelas menulis bahwa ada hit kinerja yang signifikan ketika mereka pergi ke rute yang lebih "fungsional", maka saya pasti akan memilih untuk menghindari mengandalkan inline sebagai praktik pengkodean dan sebaliknya akan mengikuti pola penulisan kode yang sedikit berbeda, tetapi sepenuhnya konsisten.

Catatan akhir tentang 'sebaris' dan definisi menjadi "dalam ruang lingkup" untuk langkah kompilasi yang terpisah. Adalah mungkin (tidak selalu dapat diandalkan) untuk pekerjaan yang harus dilakukan pada tahap menghubungkan. Ini dapat terjadi jika dan hanya jika kompiler C / C ++ mengubur detail yang cukup ke dalam file objek untuk memungkinkan linker untuk bertindak atas permintaan 'inline'. Saya pribadi belum mengalami sistem tautan (di luar Microsoft) yang mendukung kemampuan ini. Tetapi itu bisa terjadi. Sekali lagi, apakah itu harus diandalkan atau tidak akan tergantung pada keadaan. Tapi saya biasanya menganggap ini belum disekop ke linker, kecuali saya tahu sebaliknya berdasarkan bukti yang baik. Dan jika saya mengandalkannya, itu akan didokumentasikan di tempat yang menonjol.


C ++

Bagi mereka yang tertarik, berikut adalah contoh mengapa saya tetap cukup berhati-hati terhadap C ++ saat mengkode aplikasi yang disematkan, meskipun sudah tersedia saat ini. Saya akan membuang beberapa istilah yang saya pikir semua programmer C ++ tertanam perlu tahu dingin :

  • spesialisasi templat parsial
  • tabel
  • objek dasar virtual
  • bingkai aktivasi
  • bingkai aktivasi bersantai
  • penggunaan pointer cerdas dalam konstruktor, dan mengapa
  • optimalisasi nilai kembali

Itu hanya daftar pendek. Jika Anda belum mengetahui segala hal tentang persyaratan tersebut dan mengapa saya mencantumkannya (dan banyak lagi yang tidak saya sebutkan di sini) maka saya akan menyarankan agar tidak menggunakan C ++ untuk pekerjaan yang disematkan, kecuali jika itu bukan opsi untuk proyek .

Mari kita melihat semantik pengecualian C ++ untuk mendapatkan rasa.

SEBUAHB

SEBUAH

   .
   .
   foo ();
   String s;
   foo ();
   .
   .

SEBUAH

B

Kompilator C ++ melihat panggilan pertama ke foo () dan bisa membiarkan frame aktivasi yang normal terjadi, jika foo () melempar pengecualian. Dengan kata lain, kompiler C ++ tahu bahwa tidak ada kode tambahan yang diperlukan pada saat ini untuk mendukung proses pelepasan frame yang terlibat dalam penanganan pengecualian.

Tapi begitu String s telah dibuat, kompiler C ++ tahu bahwa itu harus dihancurkan dengan benar sebelum frame reasing dapat diizinkan, jika pengecualian terjadi kemudian. Jadi panggilan kedua ke foo () secara semantik berbeda dari yang pertama. Jika panggilan ke-2 untuk foo () melempar pengecualian (yang mungkin atau mungkin tidak dilakukan), kompiler harus telah menempatkan kode yang dirancang untuk menangani penghancuran String sebelum membiarkan frame biasa melepas terjadi. Ini berbeda dari kode yang diperlukan untuk panggilan pertama ke foo ().

(Dimungkinkan untuk menambahkan dekorasi tambahan dalam C ++ untuk membantu membatasi masalah ini. Tetapi kenyataannya, programmer yang menggunakan C ++ harus jauh lebih sadar akan implikasi dari setiap baris kode yang mereka tulis.)

Tidak seperti C malloc, C ++ baru menggunakan pengecualian untuk memberi sinyal ketika tidak dapat melakukan alokasi memori mentah. Begitu juga dengan 'dynamic_cast'. (Lihat edisi ke-3 Stroustrup., The C ++ Programming Language, halaman 384 dan 385 untuk pengecualian standar dalam C ++.) Compiler memungkinkan perilaku ini dinonaktifkan. Tetapi secara umum Anda akan dikenakan beberapa overhead karena pengecualian yang dibentuk dengan benar menangani prolog dan epilog dalam kode yang dihasilkan, bahkan ketika pengecualian sebenarnya tidak terjadi dan bahkan ketika fungsi yang dikompilasi tidak benar-benar memiliki pengecualian penanganan blok. (Stroustrup telah secara terbuka menyesalkan hal ini.)

Tanpa spesialisasi templat parsial (tidak semua kompiler C ++ mendukungnya), penggunaan templat dapat mengeja bencana untuk pemrograman tertanam. Tanpa itu, kode mekar adalah risiko serius yang dapat membunuh proyek memori kecil tertanam dalam sekejap.

Ketika fungsi C ++ mengembalikan objek, kompiler sementara yang tidak disebutkan namanya dibuat dan dihancurkan. Beberapa kompiler C ++ dapat memberikan kode efisien jika konstruktor objek digunakan dalam pernyataan kembali, alih-alih objek lokal, mengurangi kebutuhan konstruksi dan penghancuran oleh satu objek. Tetapi tidak setiap kompiler melakukan ini dan banyak programmer C ++ bahkan tidak menyadari "optimasi nilai pengembalian" ini.

Memberikan konstruktor objek dengan tipe parameter tunggal dapat memungkinkan kompiler C ++ untuk menemukan jalur konversi antara dua tipe dengan cara yang sama sekali tidak terduga untuk programmer. Perilaku "pintar" semacam ini bukan bagian dari C.

Klausa tangkapan yang menetapkan jenis dasar akan "mengiris" objek turunan yang dilempar, karena objek yang dilempar disalin menggunakan "tipe statis" klausa dan bukan "tipe dinamis" objek. Sumber kesengsaraan pengecualian yang tidak biasa (ketika Anda merasa Anda bahkan mampu membayar pengecualian dalam kode yang disematkan.)

Kompiler C ++ dapat secara otomatis menghasilkan konstruktor, destruktor, salin konstruktor, dan operator penugasan untuk Anda, dengan hasil yang tidak diinginkan. Butuh waktu untuk mendapatkan fasilitas dengan perincian ini.

Melewati array objek yang diturunkan ke fungsi yang menerima array objek dasar, jarang menghasilkan peringatan kompiler tetapi hampir selalu menghasilkan perilaku yang salah.

Karena C ++ tidak memanggil destruktor objek yang dibangun sebagian ketika pengecualian terjadi di konstruktor objek, penanganan pengecualian pada konstruktor biasanya mengamanatkan "smart pointer" untuk menjamin bahwa fragmen yang dibangun dalam konstruktor dihancurkan dengan benar jika pengecualian terjadi di sana . (Lihat Stroustrup, halaman 367 dan 368.) Ini adalah masalah umum dalam menulis kelas yang bagus dalam C ++, tetapi tentu saja dihindari dalam C karena C tidak memiliki semantik konstruksi dan penghancuran yang dibangun. Menulis kode yang tepat untuk menangani konstruksi sub-objek dalam suatu objek berarti menulis kode yang harus mengatasi masalah semantik unik ini di C ++; dengan kata lain "menulis sekitar" perilaku semantik C ++.

C ++ dapat menyalin objek yang diteruskan ke parameter objek. Misalnya, dalam fragmen berikut, panggilan "rA (x);" dapat menyebabkan kompiler C ++ memanggil konstruktor untuk parameter p, untuk kemudian memanggil copy constructor untuk mentransfer objek x ke parameter p, lalu konstruktor lain untuk objek kembali (sementara yang tidak disebutkan namanya) dari fungsi rA, yang tentu saja disalin dari parameter p. Lebih buruk lagi, jika kelas A memiliki objek sendiri yang membutuhkan konstruksi, ini dapat teleskop dengan bencana. (Programmer AC akan menghindari sebagian besar dari sampah ini, mengoptimalkan tangan karena programmer C tidak memiliki sintaks yang sangat berguna dan harus mengekspresikan semua detail satu per satu.)

    class A {...};
    A rA (A p) { return p; }
    // .....
    { A x; rA(x); }

Akhirnya, catatan singkat untuk programmer C. longjmp () tidak memiliki perilaku portabel di C ++. (Beberapa programmer C menggunakan ini sebagai semacam mekanisme "pengecualian".) Beberapa kompiler C ++ sebenarnya akan mencoba untuk mengatur hal-hal untuk membersihkan ketika longjmp diambil, tetapi perilaku itu tidak portabel di C ++. Jika kompiler tidak membersihkan objek yang dibangun, itu tidak portabel. Jika kompilator tidak membersihkannya, maka objek tidak rusak jika kode meninggalkan ruang lingkup objek yang dibangun sebagai akibat dari longjmp dan perilaku tidak valid. (Jika penggunaan longjmp di foo () tidak meninggalkan ruang lingkup, maka perilakunya mungkin baik-baik saja.) Ini tidak terlalu sering digunakan oleh programmer yang tertanam C tetapi mereka harus membuat diri mereka menyadari masalah ini sebelum menggunakannya.


4
Jenis fungsi yang digunakan hanya sekali tidak pernah dikompilasi sebagai pemanggilan fungsi, kode hanya ditempatkan di sana tanpa panggilan.
Dorian

6
@Dorian - komentar Anda mungkin benar dalam keadaan tertentu untuk kompiler tertentu. Jika fungsinya statis di dalam file maka kompiler memiliki opsi untuk membuat kode sebaris. jika secara eksternal terlihat maka, bahkan jika itu tidak pernah benar-benar dipanggil, harus ada cara agar fungsi tersebut dapat dipanggil.
uu

1
@jonk - Satu trik lain yang belum Anda sebutkan dalam jawaban yang baik adalah menulis fungsi makro sederhana yang melakukan inisialisasi atau konfigurasi sebagai kode inline yang diperluas. Ini sangat berguna pada prosesor yang sangat kecil di mana kedalaman panggilan RAM / stack / fungsi terbatas.
u

@ ʎəʞouɐɪ Ya, saya melewatkan diskusi makro di C. Itu sudah usang dalam C ++, tapi diskusi tentang hal itu mungkin berguna. Saya dapat mengatasinya, jika saya dapat menemukan sesuatu yang berguna untuk menulis tentang hal itu.
Jonk

1
@jonk - Saya sangat tidak setuju dengan kalimat pertama Anda. Contoh seperti inline static void turnOnFan(void) { PORTAbits &= ~(1<<8); }yang disebut di banyak tempat adalah kandidat yang sempurna.
Jason S

8

1) Kode untuk keterbacaan dan pemeliharaan pertama. Aspek yang paling penting dari basis kode apa pun adalah bahwa ia terstruktur dengan baik. Perangkat lunak yang ditulis dengan baik cenderung memiliki lebih sedikit kesalahan. Anda mungkin perlu membuat perubahan dalam beberapa minggu / bulan / tahun, dan ini sangat membantu jika kode Anda enak dibaca. Atau mungkin orang lain harus melakukan perubahan.

2) Kinerja kode yang berjalan sekali tidak terlalu menjadi masalah. Peduli gaya, bukan kinerja

3) Bahkan kode dalam loop ketat harus benar terlebih dahulu dan terutama. Jika Anda menghadapi masalah kinerja, maka optimalkan setelah kode benar.

4) Jika Anda perlu mengoptimalkan, Anda harus mengukur! Tidak masalah jika Anda berpikir atau seseorang memberitahu Anda bahwa static inlineitu hanya rekomendasi untuk kompiler. Anda harus melihat apa yang dilakukan kompiler. Anda juga harus mengukur apakah inlining meningkatkan kinerja. Dalam sistem embedded, Anda juga harus mengukur ukuran kode, karena memori kode biasanya sangat terbatas. Ini adalah aturan terpenting yang membedakan teknik dari tebakan. Jika Anda tidak mengukurnya, itu tidak membantu. Rekayasa adalah pengukuran. Sains menuliskannya;)


2
Satu-satunya kritik yang saya miliki tentang postingan Anda yang sangat bagus adalah poin 2). Memang benar bahwa kinerja kode inisialisasi tidak relevan - tetapi dalam lingkungan yang tertanam, ukurannya bisa berarti. (Tapi itu tidak mengesampingkan poin 1; mulai mengoptimalkan untuk ukuran ketika Anda perlu - dan bukan sebelumnya)
Martin Bonner mendukung Monica

2
Kinerja kode inisialisasi mungkin awalnya tidak relevan. Ketika Anda menambahkan mode daya rendah, dan ingin memulihkan dengan cepat untuk menangani acara bangun, maka itu menjadi relevan.
berendi - memprotes

5

Ketika suatu fungsi dipanggil hanya di satu tempat (bahkan di dalam fungsi lain) kompiler selalu meletakkan kode di tempat itu alih-alih benar-benar memanggil fungsi. Jika fungsi ini dipanggil di banyak tempat daripada yang masuk akal untuk menggunakan fungsi setidaknya dari sudut pandang ukuran kode.

Setelah mengkompilasi kode tidak akan memiliki beberapa panggilan saja, keterbacaan akan sangat ditingkatkan.

Anda juga akan ingin memiliki misalnya kode init ADC di perpustakaan yang sama dengan fungsi ADC lain yang tidak ada dalam file c utama.

Banyak kompiler memungkinkan Anda untuk menentukan berbagai tingkat optimasi untuk kecepatan atau ukuran kode, jadi jika Anda memiliki fungsi kecil yang dipanggil di banyak tempat maka fungsi tersebut akan "digariskan", disalin di sana alih-alih memanggil.

Optimalisasi untuk kecepatan akan menyatukan fungsi di sebanyak mungkin tempat, optimisasi untuk ukuran kode akan memanggil fungsi, namun, ketika suatu fungsi dipanggil hanya di satu tempat seperti pada kasus Anda, ia akan selalu "digariskan".

Kode seperti ini:

function_used_just_once{
   code blah blah;
}
main{
  codeblah;
  function_used_just_once();
  code blah blah blah;
{

akan dikompilasi ke:

main{
 code blah;
 code blah blah;
 code blah blah blah;
}

tanpa menggunakan panggilan apa pun.

Dan jawaban untuk pertanyaan Anda, dalam contoh Anda atau yang serupa, keterbacaan kode tidak mempengaruhi kinerja, tidak ada yang banyak dalam kecepatan atau ukuran kode. Adalah umum untuk menggunakan beberapa panggilan hanya untuk membuat kode dapat dibaca, pada akhirnya mereka dipatuhi sebagai kode inline.

Pembaruan untuk menentukan bahwa pernyataan di atas tidak berlaku untuk kompiler versi gratis yang sengaja dilumpuhkan seperti versi gratis Microchip XCxx. Panggilan fungsi semacam ini adalah tambang emas untuk Microchip untuk menunjukkan seberapa jauh lebih baik versi berbayar dan jika Anda mengompilasinya, Anda akan menemukan di ASM persis sama banyaknya panggilan yang Anda miliki dalam kode C.

Juga bukan untuk programmer bodoh yang berharap untuk menggunakan pointer ke fungsi inline.

Ini adalah bagian elektronik, bukan C ++ umum atau bagian pemrograman, pertanyaannya adalah tentang pemrograman mikrokontroler di mana setiap kompiler yang layak akan melakukan optimasi di atas secara default.

Jadi tolong hentikan pengambilan suara hanya karena dalam kasus yang jarang dan tidak biasa ini mungkin tidak benar.


15
Apakah kode menjadi inline atau tidak adalah masalah khusus implementasi vendor compiler; bahkan menggunakan kata kunci inline tidak menjamin kode inline. Ini adalah petunjuk bagi kompiler. Kompiler yang baik tentu akan sebaris fungsi yang digunakan hanya sekali jika mereka tahu tentang mereka. Ini biasanya tidak akan melakukannya jika ada objek "volatile" dalam cakupan.
Peter Smith

9
Jawaban ini tidak benar. Seperti yang dikatakan @PeterSmith, dan sesuai dengan spesifikasi bahasa C, kompiler memiliki opsi untuk memasukkan kode tetapi mungkin tidak, dan dalam banyak kasus tidak akan melakukannya. Ada begitu banyak kompiler berbeda di dunia untuk begitu banyak prosesor target berbeda yang membuat semacam pernyataan selimut dalam jawaban ini dan dengan asumsi bahwa semua kompiler akan menempatkan kode sebaris ketika mereka hanya memiliki pilihan untuk tidak dapat dipertahankan.
uu

2
@ ʎəʞouɐɪ Anda menunjukkan kasus langka di mana tidak mungkin dan itu akan menjadi ide yang buruk untuk tidak memanggil fungsi di tempat pertama. Saya belum pernah melihat kompiler yang begitu bodoh untuk benar-benar menggunakan panggilan dalam contoh sederhana yang diberikan oleh OP.
Dorian

6
Dalam kasus di mana fungsi-fungsi ini dipanggil hanya sekali, mengoptimalkan fungsi memanggil cukup banyak bukan masalah. Apakah sistem benar-benar perlu mencakar kembali setiap siklus clock tunggal selama pengaturan? Seperti halnya dengan optimasi di mana saja - tulis kode yang dapat dibaca, dan optimalkan hanya jika profil menunjukkan bahwa itu diperlukan .
Baldrickk

5
@ Pemalas Saya tidak peduli dengan apa yang akhirnya dilakukan oleh kompiler di sini - lebih pada bagaimana programmer mendekatinya. Tidak ada, atau kinerja yang dapat diabaikan dari memecah inisialisasi seperti yang terlihat dalam pertanyaan.
Baldrickk

2

Pertama, tidak ada yang terbaik atau terburuk; itu semua masalah pendapat. Anda benar bahwa ini tidak efisien. Itu bisa dioptimalkan atau tidak; tergantung. Biasanya Anda akan melihat jenis fungsi ini, jam, GPIO, timer, dll. Di file / direktori terpisah. Kompiler umumnya belum dapat mengoptimalkan celah ini. Ada satu yang bisa saya ketahui tetapi tidak banyak digunakan untuk hal-hal seperti ini.

File tunggal:

void dummy (unsigned int);

void setCLK()
{
    // Code to set the clock
    dummy(5);
}

void setConfig()
{
    // Code to set the configuration
    dummy(6);
}

void setSomethingElse()
{
   // 1 line code to write something to a register.
    dummy(7);
}

void initModule()
{
   setCLK();
   setConfig();
   setSomethingElse();
}

Memilih target dan kompiler untuk tujuan demonstrasi.

Disassembly of section .text:

00000000 <setCLK>:
   0:    e92d4010     push    {r4, lr}
   4:    e3a00005     mov    r0, #5
   8:    ebfffffe     bl    0 <dummy>
   c:    e8bd4010     pop    {r4, lr}
  10:    e12fff1e     bx    lr

00000014 <setConfig>:
  14:    e92d4010     push    {r4, lr}
  18:    e3a00006     mov    r0, #6
  1c:    ebfffffe     bl    0 <dummy>
  20:    e8bd4010     pop    {r4, lr}
  24:    e12fff1e     bx    lr

00000028 <setSomethingElse>:
  28:    e92d4010     push    {r4, lr}
  2c:    e3a00007     mov    r0, #7
  30:    ebfffffe     bl    0 <dummy>
  34:    e8bd4010     pop    {r4, lr}
  38:    e12fff1e     bx    lr

0000003c <initModule>:
  3c:    e92d4010     push    {r4, lr}
  40:    e3a00005     mov    r0, #5
  44:    ebfffffe     bl    0 <dummy>
  48:    e3a00006     mov    r0, #6
  4c:    ebfffffe     bl    0 <dummy>
  50:    e3a00007     mov    r0, #7
  54:    ebfffffe     bl    0 <dummy>
  58:    e8bd4010     pop    {r4, lr}
  5c:    e12fff1e     bx    lr

Inilah yang sebagian besar jawaban di sini memberi tahu Anda, bahwa Anda naif dan semua ini dioptimalkan dan fungsinya dihapus. Ya, mereka tidak dihapus karena secara global didefinisikan secara default. Kami dapat menghapusnya jika tidak diperlukan di luar file yang satu ini.

void dummy (unsigned int);

static void setCLK()
{
    // Code to set the clock
    dummy(5);
}

static void setConfig()
{
    // Code to set the configuration
    dummy(6);
}

static void setSomethingElse()
{
   // 1 line code to write something to a register.
    dummy(7);
}

void initModule()
{
   setCLK();
   setConfig();
   setSomethingElse();
}

menghapusnya sekarang seperti yang diuraikan.

Disassembly of section .text:

00000000 <initModule>:
   0:    e92d4010     push    {r4, lr}
   4:    e3a00005     mov    r0, #5
   8:    ebfffffe     bl    0 <dummy>
   c:    e3a00006     mov    r0, #6
  10:    ebfffffe     bl    0 <dummy>
  14:    e3a00007     mov    r0, #7
  18:    ebfffffe     bl    0 <dummy>
  1c:    e8bd4010     pop    {r4, lr}
  20:    e12fff1e     bx    lr

Tetapi kenyataannya adalah ketika Anda mengambil vendor chip atau perpustakaan BSP,

Disassembly of section .text:

00000000 <_start>:
   0:    e3a0d902     mov    sp, #32768    ; 0x8000
   4:    eb000010     bl    4c <initModule>
   8:    eafffffe     b    8 <_start+0x8>

0000000c <dummy>:
   c:    e12fff1e     bx    lr

00000010 <setCLK>:
  10:    e92d4010     push    {r4, lr}
  14:    e3a00005     mov    r0, #5
  18:    ebfffffb     bl    c <dummy>
  1c:    e8bd4010     pop    {r4, lr}
  20:    e12fff1e     bx    lr

00000024 <setConfig>:
  24:    e92d4010     push    {r4, lr}
  28:    e3a00006     mov    r0, #6
  2c:    ebfffff6     bl    c <dummy>
  30:    e8bd4010     pop    {r4, lr}
  34:    e12fff1e     bx    lr

00000038 <setSomethingElse>:
  38:    e92d4010     push    {r4, lr}
  3c:    e3a00007     mov    r0, #7
  40:    ebfffff1     bl    c <dummy>
  44:    e8bd4010     pop    {r4, lr}
  48:    e12fff1e     bx    lr

0000004c <initModule>:
  4c:    e92d4010     push    {r4, lr}
  50:    ebffffee     bl    10 <setCLK>
  54:    ebfffff2     bl    24 <setConfig>
  58:    ebfffff6     bl    38 <setSomethingElse>
  5c:    e8bd4010     pop    {r4, lr}
  60:    e12fff1e     bx    lr

Anda pasti akan mulai menambahkan overhead, yang memiliki biaya nyata untuk kinerja dan ruang. Beberapa hingga lima persen dari masing-masing tergantung pada seberapa kecil fungsi masing-masing.

Mengapa ini dilakukan? Beberapa di antaranya adalah seperangkat aturan yang akan atau masih diajarkan profesor untuk membuat kode penilaian lebih mudah. Fungsinya harus sesuai pada halaman (saat Anda mencetak karya Anda di atas kertas), jangan lakukan ini, jangan lakukan itu, dll. Banyak yang membuat perpustakaan dengan nama umum untuk target yang berbeda. Jika Anda memiliki puluhan keluarga mikrokontroler, beberapa di antaranya berbagi periferal dan beberapa tidak, mungkin tiga atau empat rasa UART berbeda dicampur di antara keluarga, GPIO yang berbeda, pengontrol SPI, dll. Anda dapat memiliki fungsi gpio_init () yang umum, get_timer_count (), dll. Dan gunakan kembali abstraksi tersebut untuk periferal yang berbeda.

Ini menjadi kasus sebagian besar rawatan dan desain perangkat lunak, dengan beberapa kemungkinan keterbacaan. Maintabilitas, keterbacaan, dan kinerja Anda tidak bisa memiliki semuanya; Anda hanya dapat memilih satu atau dua sekaligus, bukan ketiganya.

Ini sangat banyak pertanyaan berbasis opini, dan di atas menunjukkan tiga cara utama ini bisa pergi. Seperti apa jalan TERBAIK yang sepenuhnya merupakan pendapat. Apakah melakukan semua pekerjaan dalam satu fungsi? Pertanyaan berbasis opini, beberapa orang cenderung untuk kinerja, beberapa mendefinisikan modularitas dan versi keterbacaan mereka sebagai TERBAIK. Masalah menarik dengan apa yang oleh banyak orang disebut keterbacaan sangat menyakitkan; untuk "melihat" kode Anda harus membuka 50-10.000 file sekaligus dan entah bagaimana mencoba melihat fungsi-fungsi dalam urutan eksekusi untuk melihat apa yang sedang terjadi. Saya menemukan bahwa kebalikan dari keterbacaan, tetapi yang lain menemukannya dapat dibaca karena setiap item cocok di layar / editor jendela dan dapat dikonsumsi secara keseluruhan setelah mereka menghafal fungsi yang dipanggil dan / atau memiliki editor yang dapat keluar masuk dari setiap fungsi dalam suatu proyek.

Itu adalah faktor besar lain ketika Anda melihat berbagai solusi. Editor teks, IDE, dll. Sangat pribadi, dan melampaui vi vs Emacs. Pemrograman efisiensi, garis per hari / bulan naik jika Anda merasa nyaman dan efisien dengan alat yang Anda gunakan. Fitur-fitur alat ini dapat / akan dengan sengaja atau tidak condong ke arah bagaimana penggemar alat itu menulis kode. Dan sebagai hasilnya jika seseorang menulis perpustakaan-perpustakaan ini, proyek tersebut sampai batas tertentu mencerminkan kebiasaan-kebiasaan ini. Bahkan jika itu adalah sebuah tim, kebiasaan / preferensi pemimpin pengembang atau bos mungkin dipaksa ke anggota tim lainnya.

Standar pengkodean yang memiliki banyak preferensi pribadi terkubur di dalamnya, sangat religius vs Emacs lagi, tab vs spasi, bagaimana tanda kurung berbaris, dll. Dan ini menunjukkan bagaimana perpustakaan dirancang sampai batas tertentu.

Bagaimana seharusnya Anda menulis milik Anda? Bagaimanapun yang Anda inginkan, sebenarnya tidak ada jawaban yang salah jika berfungsi. Tentu ada kode yang buruk atau berisiko, tetapi jika ditulis sedemikian rupa sehingga Anda dapat mempertahankannya sesuai kebutuhan, itu memenuhi tujuan desain Anda, menyerah pada keterbacaan dan beberapa pemeliharaan jika kinerja penting, atau sebaliknya. Apakah Anda suka nama variabel pendek sehingga satu baris kode sesuai dengan lebar jendela editor? Atau nama yang terlalu deskriptif panjang untuk menghindari kebingungan, tetapi keterbacaan menurun karena Anda tidak bisa mendapatkan satu baris pada satu halaman; sekarang secara visual putus, mengacaukan aliran.

Anda tidak akan mencapai home run pertama kali di kelelawar. Mungkin perlu / harus puluhan tahun untuk benar-benar mendefinisikan gaya Anda. Pada saat yang sama, dari waktu ke waktu, gaya Anda mungkin berubah, condong satu arah untuk sementara waktu, lalu condong ke arah lain.

Anda akan mendengar banyak dari tidak mengoptimalkan, tidak pernah mengoptimalkan, dan optimasi prematur. Tetapi seperti yang ditunjukkan, desain seperti ini dari awal menciptakan masalah kinerja, kemudian Anda mulai melihat peretasan untuk menyelesaikan masalah itu alih-alih mendesain ulang dari awal untuk melakukan. Saya setuju ada beberapa situasi, satu fungsi beberapa baris kode yang Anda dapat berusaha keras untuk memanipulasi kompiler berdasarkan pada ketakutan apa yang akan dilakukan kompiler sebaliknya (perhatikan dengan pengalaman pengkodean semacam ini menjadi mudah dan alami, mengoptimalkan ketika Anda menulis mengetahui bagaimana kompiler akan mengkompilasi kode), maka Anda ingin mengkonfirmasi di mana pencuri siklus sebenarnya, sebelum menyerang itu.

Anda juga perlu merancang kode Anda untuk pengguna sampai batas tertentu. Jika ini adalah proyek Anda, Anda adalah satu-satunya pengembang; apapun yang kamu inginkan. Jika Anda mencoba membuat perpustakaan untuk diberikan atau dijual, Anda mungkin ingin membuat kode Anda terlihat seperti semua perpustakaan lain, ratusan hingga ribuan file dengan fungsi kecil, nama fungsi panjang, dan nama variabel panjang. Terlepas dari masalah keterbacaan dan masalah kinerja, IMO Anda akan menemukan lebih banyak orang akan dapat menggunakan kode itu.


4
Betulkah? Apa "target" dan "kompiler" apa yang Anda gunakan yang bisa saya tanyakan?
Dorian

Sepertinya saya lebih seperti ARM8 32/64, mungkin dari raspbery PI kemudian mikrokontroler biasa. Sudahkah Anda membaca kalimat pertama dalam pertanyaan?
Dorian

Ya, kompiler tidak menghapus fungsi global yang tidak digunakan, tetapi tautannya tidak. Jika itu dikonfigurasi dan digunakan dengan benar, mereka tidak akan muncul di executable.
berendi - memprotes

Jika seseorang bertanya-tanya kompiler mana yang dapat mengoptimalkan celah antar file: kompiler IAR mendukung kompilasi multi file (begitulah mereka menyebutnya), yang memungkinkan untuk optimasi file silang. Jika Anda membuang semua file c / cpp dalam sekali jalan Anda berakhir dengan executable yang berisi fungsi tunggal: main. Manfaat kinerja bisa sangat mendalam.
Arsenal

3
@Arsenal Tentu saja gcc mendukung inlining, bahkan di seluruh unit kompilasi jika dipanggil dengan benar. Lihat gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html dan cari opsi -flto.
Peter - Reinstate Monica

1

Aturan yang sangat umum - kompiler dapat mengoptimalkan lebih baik daripada Anda. Tentu saja, ada pengecualian jika Anda melakukan hal-hal yang sangat intensif, tetapi secara keseluruhan jika Anda menginginkan optimasi yang baik untuk kecepatan atau ukuran kode, pilih kompiler Anda dengan bijak.


Sayangnya itu berlaku untuk sebagian besar programmer hari ini.
Dorian

0

Itu pasti tergantung pada gaya pengkodean Anda sendiri. Satu aturan umum yang ada di luar sana adalah, bahwa nama variabel serta nama fungsi harus sejelas dan sejelas mungkin menjelaskan sendiri. Semakin banyak sub-panggilan atau baris kode yang Anda masukkan ke dalam suatu fungsi, semakin sulit untuk mendefinisikan tugas yang jelas untuk fungsi yang satu itu. Dalam contoh Anda, Anda memiliki fungsi initModule()yang menginisialisasi hal-hal dan memanggil sub-rutin yang kemudian mengatur jam atau mengatur konfigurasi . Anda dapat mengetahui itu hanya dengan membaca nama fungsi. Jika Anda meletakkan semua kode dari subrutin di komputer Anda initModule()secara langsung, itu menjadi kurang jelas apa fungsi sebenarnya. Tapi seperti sering, itu hanya panduan.


Terimakasih atas balasan anda. Saya mungkin mengubah gaya jika diperlukan untuk kinerja tetapi pertanyaan di sini adalah apakah keterbacaan kode mempengaruhi kinerja?
MaNyYaCk

Panggilan fungsi akan menghasilkan panggilan atau perintah jmp tapi itu pengorbanan sumber daya diabaikan menurut pendapat saya. Jika Anda menggunakan pola desain Anda kadang-kadang berakhir dengan selusin lapisan panggilan fungsi sebelum Anda mencapai kode yang sebenarnya terpotong.
po.pe

@Humpawumpa - Jika Anda menulis untuk mikrokontroler dengan hanya 256 atau 64 byte RAM maka selusin lapisan pemanggilan fungsi bukan pengorbanan yang dapat diabaikan, hanya saja tidak mungkin
u

Ya, tetapi ini adalah dua ekstrem ... biasanya Anda memiliki lebih dari 256 byte dan menggunakan kurang dari selusin lapisan - semoga.
po.pe

0

Jika suatu fungsi benar-benar hanya melakukan satu hal yang sangat kecil, pertimbangkan membuatnya static inline.

Tambahkan ke file header bukan file C, dan gunakan kata-kata static inlineuntuk mendefinisikannya:

static inline void setCLK()
{
    //code to set the clock
}

Sekarang, jika fungsinya bahkan sedikit lebih lama, seperti menjadi lebih dari 3 baris, mungkin ide yang baik untuk menghindari static inlinedan menambahkannya ke file .c. Bagaimanapun, sistem tertanam memiliki memori terbatas, dan Anda tidak ingin menambah ukuran kode terlalu banyak.

Juga, jika Anda mendefinisikan fungsi dalam file1.cdan menggunakannya dari file2.c, kompiler tidak akan secara otomatis menyejajarkannya. Namun, jika Anda mendefinisikannya file1.hsebagai suatu static inlinefungsi, kemungkinan kompiler Anda mengikutinya.

static inlineFungsi - fungsi ini sangat berguna dalam pemrograman kinerja tinggi. Saya telah menemukan mereka untuk meningkatkan kinerja kode sering dengan faktor lebih dari tiga.


"seperti berada di atas 3 baris" - jumlah baris tidak ada hubungannya dengan itu; biaya inlining ada hubungannya dengan itu. Saya bisa menulis fungsi 20-line yang sempurna untuk inlining, dan fungsi 3-line yang mengerikan untuk inlining (misalnya functionA () yang memanggil functionB () 3 kali, functionB () yang memanggil functionC () 3 kali, dan beberapa level lainnya).
Jason S

Juga, jika Anda mendefinisikan fungsi dalam file1.cdan menggunakannya dari file2.c, kompiler tidak akan secara otomatis menyejajarkannya. Salah . Lihat misalnya -fltodi gcc atau dentang.
berendi - memprotes

0

Satu kesulitan dengan mencoba menulis kode yang efisien dan andal untuk mikrokontroler adalah bahwa beberapa kompiler tidak dapat menangani semantik tertentu dengan andal kecuali kode menggunakan arahan khusus kompiler atau menonaktifkan banyak optimisasi.

Misalnya, jika memiliki sistem inti tunggal dengan rutinitas layanan interupsi [dijalankan oleh kutu pengatur waktu atau apa pun]:

volatile uint32_t *magic_write_ptr,magic_write_count;
void handle_interrupt(void)
{
  if (magic_write_count)
  {
    magic_write_count--;
    send_data(*magic_write_ptr++)
  }
}

seharusnya dimungkinkan untuk menulis fungsi untuk memulai operasi penulisan latar belakang atau menunggu sampai selesai:

void wait_for_background_write(void)
{
  while(magic_write_count)
    ;
}
void start_background_write(uint32_t *dat, uint32_t count)
{
  wait_for_background_write();
  background_write_ptr = dat;
  background_write_count = count;
}

dan kemudian panggil kode tersebut menggunakan:

uint32_t buff[16];

... write first set of data into buff
start_background_write(buff, 16);
... do some stuff unrelated to buff
wait_for_background_write();

... write second set of data into buff
start_background_write(buff, 16);
... etc.

Sayangnya, dengan optimalisasi penuh diaktifkan, kompiler "pintar" seperti gcc atau dentang akan memutuskan bahwa tidak ada cara set pertama menulis dapat memiliki efek pada diamati dari program dan dengan demikian mereka dapat dioptimalkan. Penyusun kualitas seperti icccenderung tidak melakukan hal ini jika tindakan menetapkan penyelesaian interupsi dan menunggu melibatkan penulisan yang tidak stabil dan pembacaan tidak stabil (seperti halnya di sini), tetapi platform yang ditargetkan olehicc tidak begitu populer untuk sistem embedded.

Standar sengaja mengabaikan masalah kualitas implementasi, memperkirakan bahwa ada beberapa cara masuk akal konstruk di atas dapat ditangani:

  1. Implementasi kualitas yang ditujukan khusus untuk bidang-bidang seperti pengelompokan angka kelas atas dapat beralasan mengharapkan bahwa kode yang ditulis untuk bidang semacam itu tidak akan mengandung konstruksi seperti di atas.

  2. Implementasi kualitas dapat memperlakukan semua akses ke volatileobjek seolah-olah mereka dapat memicu tindakan yang akan mengakses objek apa pun yang terlihat oleh dunia luar.

  3. Implementasi sederhana namun berkualitas baik yang ditujukan untuk penggunaan sistem tertanam mungkin memperlakukan semua panggilan ke fungsi yang tidak ditandai "inline" seolah-olah mereka dapat mengakses objek apa pun yang telah terpapar ke dunia luar, bahkan jika itu tidak memperlakukan volatileseperti yang dijelaskan dalam # 2.

Standar ini tidak berupaya untuk menyarankan pendekatan mana di atas yang paling sesuai untuk implementasi kualitas, atau untuk mengharuskan implementasi yang "sesuai" memiliki kualitas yang cukup baik untuk dapat digunakan untuk tujuan tertentu. Akibatnya, beberapa kompiler seperti gcc atau dentang secara efektif mensyaratkan bahwa setiap kode yang ingin menggunakan pola ini harus dikompilasi dengan banyak optimasi yang dinonaktifkan.

Dalam beberapa kasus, memastikan bahwa fungsi I / O berada dalam unit kompilasi yang terpisah dan kompiler tidak akan memiliki pilihan selain berasumsi mereka dapat mengakses setiap subset objek yang sewenang-wenang yang telah terpapar ke dunia luar mungkin paling tidak masuk akal- of-Evils cara menulis kode yang akan bekerja andal dengan gcc dan dentang. Namun, dalam kasus seperti itu, tujuannya bukan untuk menghindari biaya tambahan dari panggilan fungsi yang tidak perlu, tetapi untuk menerima biaya yang seharusnya tidak perlu dengan imbalan mendapatkan semantik yang diperlukan.


"Memastikan bahwa fungsi I / O berada dalam unit kompilasi terpisah" ... bukan cara ampuh untuk mencegah masalah optimisasi seperti ini. Setidaknya LLVM dan saya percaya GCC akan melakukan optimasi seluruh-program dalam banyak kasus, jadi dapat memutuskan untuk menyesuaikan fungsi IO Anda bahkan jika mereka berada di unit kompilasi yang terpisah.
Jules

@ Jules: Tidak semua implementasi cocok untuk menulis perangkat lunak yang disematkan. Menonaktifkan pengoptimalan seluruh program mungkin merupakan cara yang paling murah untuk memaksa gcc atau clang berperilaku sebagai implementasi kualitas yang sesuai untuk tujuan itu.
supercat

@ Jules: Implementasi berkualitas tinggi yang ditujukan untuk embedded atau pemrograman sistem harus dapat dikonfigurasi untuk memiliki semantik yang sesuai untuk tujuan itu tanpa harus sepenuhnya menonaktifkan optimasi seluruh program (misalnya dengan memiliki opsi untuk memperlakukan volatileakses seolah-olah mereka berpotensi memicu akses sewenang-wenang ke objek lain), tetapi untuk alasan apa pun gcc dan dentang lebih suka memperlakukan masalah kualitas implementasi sebagai undangan untuk berperilaku dengan cara yang tidak berguna.
supercat

1
Bahkan implementasi "kualitas tertinggi" tidak akan memperbaiki kode buggy. Jika bufftidak dinyatakan volatile, itu tidak akan diperlakukan sebagai variabel volatil, akses ke sana dapat disusun ulang atau dioptimalkan sepenuhnya jika tampaknya tidak digunakan nanti. Aturannya sederhana: tandai semua variabel yang mungkin diakses di luar aliran program normal (seperti yang dilihat oleh kompiler) sebagai volatile. Apakah konten buffdiakses dalam interrupt handler? Iya. Maka seharusnya volatile.
berendi - memprotes

@berendi: Penyusun dapat menawarkan jaminan di luar apa yang disyaratkan Standar dan penyusun kualitas akan melakukannya. Implementasi berdiri bebas kualitas untuk penggunaan sistem embedded akan memungkinkan programmer untuk mensintesis konstruksi mutex, yang pada dasarnya adalah apa yang dilakukan kode. Ketika magic_write_countnol, penyimpanan dimiliki oleh jalur utama. Ketika bukan nol, itu dimiliki oleh pengendali interupsi. Membuat buffvolatile akan mengharuskan setiap fungsi di mana saja yang beroperasi menggunakan volatilepointer-memenuhi syarat, yang akan mengganggu optimasi jauh lebih banyak daripada memiliki kompiler ...
supercat
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.