Array atau Malloc?

12

Saya menggunakan kode berikut dalam aplikasi saya, dan itu berfungsi dengan baik. Tapi saya bertanya-tanya apakah lebih baik membuatnya dengan malloc atau membiarkannya apa adanya?

function (int len)
{
char result [len] = some chars;
send result over network
}
Dev Bag
sumber
2
Apakah asumsi bahwa kode ditargetkan untuk lingkungan yang tidak tertanam?
tehnyit
stackoverflow.com/questions/16672322/…
Ciro Santilli 新疆 改造 中心 法轮功 六四 事件

Jawaban:

27

Perbedaan utama adalah bahwa VLA (array panjang variabel) tidak menyediakan mekanisme untuk mendeteksi kegagalan alokasi.

Jika Anda menyatakan

char result[len];

dan lenmelebihi jumlah ruang tumpukan yang tersedia, perilaku program Anda tidak ditentukan. Tidak ada mekanisme bahasa baik untuk menentukan terlebih dahulu apakah alokasi akan berhasil, atau untuk menentukan setelah fakta apakah itu berhasil.

Di sisi lain, jika Anda menulis:

char *result = malloc(len);
if (result == NULL) {
    /* allocation failed, abort or take corrective action */
}

maka Anda dapat menangani kegagalan dengan anggun, atau setidaknya menjamin bahwa program Anda tidak akan mencoba untuk terus mengeksekusi setelah kegagalan.

(Yah, sebagian besar. Pada sistem Linux, malloc()dapat mengalokasikan sejumlah ruang alamat bahkan jika tidak ada penyimpanan yang sesuai tersedia; kemudian upaya untuk menggunakan ruang tersebut dapat memanggil Pembunuh OOM . Tetapi memeriksa malloc()kegagalan masih merupakan praktik yang baik.)

Masalah lain, pada banyak sistem, adalah bahwa ada lebih banyak ruang (mungkin lebih banyak ruang) tersedia malloc()daripada untuk objek otomatis seperti VLA.

Dan seperti jawaban Philip sudah disebutkan, VLA ditambahkan dalam C99 (Microsoft khususnya tidak mendukung mereka).

Dan VLA dibuat opsional di C11. Mungkin sebagian besar kompiler C11 akan mendukungnya, tetapi Anda tidak dapat mengandalkannya.

Keith Thompson
sumber
14

Array otomatis panjang variabel diperkenalkan ke C di C99.

Kecuali jika Anda memiliki kekhawatiran tentang keterbandingan mundur dengan standar yang lebih lama, tidak masalah.

Secara umum, jika berhasil, jangan menyentuhnya. Jangan mengoptimalkan sebelumnya. Jangan khawatir tentang menambahkan fitur khusus atau cara pintar dalam melakukan sesuatu, karena Anda sering tidak akan menggunakannya. Tetap sederhana.

Philip
sumber
7
Saya harus tidak setuju dengan diktum "jika berhasil, jangan sentuh". Salah mempercayai bahwa beberapa kode "berfungsi" dapat menyebabkan Anda mengatasi masalah dalam beberapa kode yang "berfungsi". Keyakinan harus diganti dengan penerimaan sementara bahwa beberapa kode berfungsi saat ini.
Bruce Ediger
2
Jangan menyentuhnya sampai Anda membuat versi yang mungkin "berfungsi" lebih baik ...
H_7
8

Jika kompiler Anda mendukung array panjang variabel, satu-satunya bahaya adalah meluap tumpukan pada beberapa sistem, ketika lenitu sangat besar. Jika Anda tahu pasti bahwa lenitu tidak akan lebih besar dari angka tertentu, dan Anda tahu bahwa tumpukan Anda tidak akan meluap bahkan pada panjang maksimal, biarkan kode seperti apa adanya; jika tidak, tulis ulang dengan mallocdan free.

dasblinkenlight
sumber
bagaimana dengan ini pada fungsi non c99 (char []) {char result [sizeof (char)] = beberapa karakter; kirim hasil melalui jaringan}
Dev Bag
@ DevBag char result [sizeof(char)]adalah array ukuran 1(karena sizeof(char)sama dengan satu), jadi tugasnya akan terpotong some chars.
dasblinkenlight
maaf soal itu, maksud saya begini fungsi (char str []) {char result [sizeof (str)] = beberapa karakter; kirim hasil melalui jaringan}
Dev Bag
4
@ DavidBag Ini tidak akan berfungsi baik - str meluruh ke pointer , jadi sizeofitu akan menjadi empat atau delapan, tergantung pada ukuran pointer pada sistem Anda.
dasblinkenlight
2
Jika Anda menggunakan versi C tanpa array panjang variabel, Anda mungkin bisa melakukannya char* result = alloca(len);, yang mengalokasikan pada stack. Ini memiliki efek dasar yang sama (dan masalah dasar yang sama)
Gort the Robot
6

Saya suka ide bahwa Anda dapat memiliki array run-time yang dialokasikan tanpa fragmentasi memori, pointer menggantung, dll. Namun, yang lain telah menunjukkan bahwa alokasi run-time ini bisa gagal gagal. Jadi saya mencoba ini menggunakan gcc 4.5.3 di lingkungan bash Cygwin:

#include <stdio.h>
#include <string.h>

void testit (unsigned long len)
{
    char result [len*2];
    char marker[100];

    memset(marker, 0, sizeof(marker));
    printf("result's size: %lu\n", sizeof(result));
    strcpy(result, "this is a test that should overflow if no allocation");
    printf("marker's contents: '%s'\n", marker);
}

int main(int argc, char *argv[])
{
    testit(100);
    testit((unsigned long)-1);  // probably too big
}

Outputnya adalah:

$ ./a.exe
result's size: 200
marker's contents: ''
result's size: 4294967294
marker's contents: 'should overflow if no allocation'

Panjang yang terlalu besar yang dilewati pada panggilan kedua jelas menyebabkan kegagalan (meluap ke penanda []). Ini tidak berarti bahwa cek semacam ini adalah bukti bodoh (bodoh bisa pintar!) Atau bahwa itu memenuhi standar C99, tetapi mungkin membantu jika Anda memiliki kekhawatiran itu.

Seperti biasa, YMMV.

Harold Bamford
sumber
1
+1 ini sangat berguna: 3
Kokizzu
Selalu menyenangkan untuk memiliki beberapa kode yang sesuai dengan klaim yang dibuat orang! Terima kasih ^ _ ^
Musa Al-hassy
3

Secara umum tumpukan adalah tempat termudah dan terbaik untuk menaruh data Anda.

Saya akan menghindari masalah VLA dengan hanya mengalokasikan array terbesar yang Anda harapkan.

Namun ada beberapa kasus ketika tumpukan terbaik dan bermain-main dengan malloc layak dilakukan.

  1. Ketika jumlah data yang besar tetapi variabel. Besar tergantung pada lingkungan Anda> 1K untuk sistem tertanam,> 10MB untuk server Enterprise.
  2. Ketika Anda ingin data tetap ada setelah Anda keluar dari rutinitas Anda, misalnya jika Anda mengembalikan pointer ke data Anda. Menggunakan
  3. Kombinasi pointer statis dan malloc () biasanya lebih baik daripada mendefinisikan array statis besar;
James Anderson
sumber
3

Dalam pemrograman tertanam, kami selalu menggunakan array statis alih-alih malloc ketika malloc dan operasi bebas sering dilakukan. Karena kurangnya manajemen memori dalam sistem embedded, alokasi yang sering dan operasi bebas akan menyebabkan fragmen memori. Tetapi kita harus menggunakan beberapa metode rumit seperti mendefinisikan ukuran maksimum array dan menggunakan array lokal statis.

Jika aplikasi Anda berjalan di Linux atau Windows, tidak masalah menggunakan array atau malloc. Poin kuncinya adalah di mana Anda menggunakan struktur tanggal dan logika kode Anda.

Steven Mou
sumber
1

Sesuatu yang belum ada yang disebutkan adalah bahwa opsi array panjang variabel mungkin akan jauh lebih cepat daripada malloc / gratis karena mengalokasikan VLA hanyalah kasus menyesuaikan stack pointer (setidaknya di GCC).

Jadi, jika fungsi ini adalah yang sering dipanggil (yang tentunya akan Anda tentukan dengan membuat profil), VLA adalah opsi pengoptimalan yang baik.

JeremyP
sumber
1
Tampaknya bagus sampai pada titik ketika mendorong Anda ke dalam situasi out-of-stack-space. Terlebih lagi, itu mungkin bukan kode Anda yang benar - benar mencapai batas tumpukan; itu bisa berakhir dengan menggigit perpustakaan atau panggilan sistem (atau interupsi).
Donal Fellows
@Donal Performance selalu merupakan pertukaran memori dengan kecepatan. Jika Anda akan berkeliling mengalokasikan array beberapa megabyte, maka Anda ada benarnya, namun, bahkan untuk beberapa kilobyte, selama fungsinya tidak rekursif, itu adalah optimasi yang baik.
JeremyP
1

Ini adalah solusi C yang sangat umum saya gunakan untuk masalah yang mungkin bisa membantu. Tidak seperti VLA, VLA tidak menghadapi risiko praktis stack overflow dalam kasus patologis.

/// Used for frequent allocations where the common case generally allocates
/// a small amount of memory, at which point a heap allocation can be
/// avoided, but rare cases also need to be handled which may allocate a
/// substantial amount. Note that this structure is not safe to copy as
/// it could potentially invalidate the 'data' pointer. Its primary use
/// is just to allow the stack to be used in common cases.
struct FastMem
{
    /// Stores raw bytes for fast access.
    char fast_mem[512];

    /// Points to 'fast_mem' if the data fits. Otherwise, it will point to a
    /// dynamically allocated memory address.
    void* data;
};

/// @return A pointer to a newly allocated memory block of the specified size.
/// If the memory fits in the specified fast memory structure, it will use that
/// instead of the heap.
void* fm_malloc(struct FastMem* mem, int size)
{
    // Utilize the stack if the memory fits, otherwise malloc.
    mem->data = (size < sizeof mem->fast_mem) ? mem->fast_mem: malloc(size);
    return mem->data;
}

/// Frees the specified memory block if it has been allocated on the heap.
void fm_free(struct FastMem* mem)
{
    // Free the memory if it was allocated dynamically with 'malloc'.
    if (mem->data != mem->fast_mem)
        free(mem->data);
    mem->data = 0;
}

Untuk menggunakannya dalam kasus Anda:

struct FastMem fm;

// `result` will be allocated on the stack if 'len <= 512'.
char* result = fm_malloc(&fm, len);

// send result over network.
...

// this function will only do a heap deallocation if 'len > 512'.
fm_free(&fm, result);

Apa yang dilakukan dalam kasus di atas adalah menggunakan stack jika string cocok menjadi 512 byte atau kurang. Kalau tidak, ia menggunakan alokasi tumpukan. Ini dapat berguna jika, katakanlah, 99% dari waktu, string tersebut cocok menjadi 512 byte atau kurang. Namun, katakanlah ada beberapa kasus eksotis gila yang kadang-kadang Anda mungkin perlu menangani di mana string adalah 32 kilobyte di mana pengguna tertidur di keyboard-nya atau sesuatu seperti itu. Ini memungkinkan kedua situasi ditangani tanpa masalah.

Versi aktual yang saya gunakan dalam produksi juga memiliki versi sendiri reallocdan callocdan seterusnya serta struktur data C ++ yang sesuai standar yang dibangun pada konsep yang sama, tetapi saya mengekstrak minimum yang diperlukan untuk menggambarkan konsep tersebut.

Memang ada peringatan bahwa itu berbahaya untuk disalin dan Anda tidak boleh mengembalikan pointer yang dialokasikan melaluinya (mereka bisa berakhir tidak valid karena FastMeminstance dihancurkan). Ini dimaksudkan untuk digunakan untuk kasus-kasus sederhana dalam lingkup fungsi lokal di mana Anda akan tergoda untuk selalu menggunakan stack / VLA sebaliknya jika beberapa kasus yang jarang terjadi dapat menyebabkan buffer / stack overflow. Ini bukan pengalokasi tujuan umum dan tidak boleh digunakan seperti itu.

Saya benar-benar menciptakannya beberapa waktu lalu sebagai tanggapan terhadap situasi dalam basis kode lama menggunakan C89 bahwa mantan tim berpikir tidak akan pernah terjadi di mana pengguna berhasil menamai item dengan nama yang lebih dari 2.047 karakter (mungkin dia tertidur di keyboard-nya) ). Rekan-rekan saya benar-benar mencoba untuk meningkatkan ukuran array yang dialokasikan di berbagai tempat menjadi 16.384 sebagai respons di mana saya pikir itu semakin konyol dan hanya menukar risiko stack overflow yang lebih besar dengan imbalan risiko buffer overflow yang lebih rendah. Ini memberikan solusi yang sangat mudah dipasang untuk memperbaiki kasus-kasus itu dengan hanya menambahkan beberapa baris kode. Hal ini memungkinkan kasus umum ditangani dengan sangat efisien dan masih menggunakan tumpukan tanpa kasus-kasus langka gila yang menuntut tumpukan perangkat lunak. Namun, saya telah menemukannya bermanfaat sejak saat itu bahkan setelah C99 karena VLA masih tidak dapat melindungi kita terhadap tumpukan berlebihan. Yang ini bisa tetapi masih menyatu dari tumpukan untuk permintaan alokasi kecil.


sumber
1

The Stack panggilan selalu terbatas. Pada sistem operasi utama seperti Linux atau Windows, batasnya adalah satu atau beberapa megabyte (dan Anda bisa menemukan cara untuk mengubahnya). Dengan beberapa aplikasi multi-utas, ini bisa lebih rendah (karena utas dapat dibuat dengan tumpukan yang lebih kecil). Pada sistem embedded, ukurannya bisa sekecil beberapa kilobyte. Aturan praktis yang baik adalah untuk menghindari frame panggilan yang lebih besar dari beberapa kilobyte.

Jadi menggunakan VLA masuk akal hanya jika Anda yakin bahwa Anda lencukup kecil (paling banyak beberapa puluh ribu). Kalau tidak, Anda memiliki tumpukan overflow dan itu adalah kasus perilaku yang tidak terdefinisi , situasi yang sangat menakutkan .

Namun, dengan menggunakan panduan alokasi memori C dinamis (misalnya callocatau malloc&free ) juga memiliki kekurangan:

  • itu bisa gagal dan Anda harus selalu menguji kegagalan (misalnya callocatau mallockembali NULL).

  • itu lebih lambat: alokasi VLA yang sukses membutuhkan beberapa nanodetik, yang berhasil mallocbisa memerlukan beberapa mikrodetik (dalam kasus yang baik, hanya sebagian kecil dari mikrodetik) atau bahkan lebih (dalam kasus patologis yang melibatkan meronta - ronta , lebih banyak lagi).

  • jauh lebih sulit untuk dikodekan: Anda freehanya bisa ketika Anda yakin bahwa zona runcing tidak lagi digunakan. Dalam kasus Anda, Anda dapat menghubungi keduanya callocdan freedalam rutinitas yang sama.

Jika Anda tahu bahwa sebagian besar waktu Anda result (nama yang sangat buruk, Anda tidak boleh mengembalikan alamat variabel VLA otomatis ; jadi saya menggunakan bufalih-alih di resultbawah) kecil, Anda dapat menggunakan huruf besar, misalnya

char tinybuf[256];
char *buf = (len<sizeof(tinybuf))?tinybuf:malloc(len);
if (!buf) { perror("malloc"); exit(EXIT_FAILURE); };
fill_buffer(buf, len);
send_buffer_on_network(buf, len);
if (buf != tinybuf) 
  free(buf);

Namun, kode di atas kurang dapat dibaca dan mungkin optimasi prematur. Namun itu lebih kuat daripada solusi VLA murni.

PS. Beberapa sistem (misalnya beberapa distribusi Linux diaktifkan secara default) memiliki overcommitment memori (yang membuat mallocmemberikan beberapa pointer bahkan jika tidak ada cukup memori). Ini adalah fitur yang saya sukai dan biasanya dinonaktifkan pada mesin Linux saya.

Basile Starynkevitch
sumber