Apakah ada kerugian untuk melewati struct dengan nilai dalam C, daripada melewati pointer?

157

Apakah ada kerugian untuk melewati struct dengan nilai dalam C, daripada melewati pointer?

Jika struct besar, jelas ada aspek performansi menyalin banyak data, tetapi untuk struct lebih kecil, pada dasarnya harus sama dengan melewatkan beberapa nilai ke suatu fungsi.

Itu mungkin bahkan lebih menarik ketika digunakan sebagai nilai pengembalian. C hanya memiliki nilai pengembalian tunggal dari fungsi, tetapi Anda sering membutuhkan beberapa. Jadi solusi sederhana adalah dengan meletakkannya di struct dan mengembalikannya.

Apakah ada alasan untuk atau menentang ini?

Karena mungkin tidak jelas bagi semua orang apa yang saya bicarakan di sini, saya akan memberikan contoh sederhana.

Jika Anda memprogram dalam C, cepat atau lambat Anda akan mulai menulis fungsi yang terlihat seperti ini:

void examine_data(const char *ptr, size_t len)
{
    ...
}

char *p = ...;
size_t l = ...;
examine_data(p, l);

Ini bukan masalah. Satu-satunya masalah adalah bahwa Anda harus setuju dengan rekan kerja Anda di mana urutan parameter harus jadi Anda menggunakan konvensi yang sama di semua fungsi.

Tetapi apa yang terjadi ketika Anda ingin mengembalikan informasi yang sama? Anda biasanya mendapatkan sesuatu seperti ini:

char *get_data(size_t *len);
{
    ...
    *len = ...datalen...;
    return ...data...;
}
size_t len;
char *p = get_data(&len);

Ini berfungsi dengan baik, tetapi jauh lebih bermasalah. Nilai kembali adalah nilai balik, kecuali bahwa dalam implementasi ini tidak. Tidak ada cara untuk mengatakan dari atas bahwa fungsi get_data tidak diizinkan untuk melihat apa yang ditunjukkan oleh len. Dan tidak ada yang membuat kompiler memeriksa apakah suatu nilai benar-benar dikembalikan melalui pointer itu. Jadi bulan depan, ketika orang lain memodifikasi kode tanpa memahaminya dengan benar (karena dia tidak membaca dokumentasi?) Itu rusak tanpa ada yang memperhatikan, atau mulai crash secara acak.

Jadi, solusi yang saya usulkan adalah struct sederhana

struct blob { char *ptr; size_t len; }

Contoh-contoh dapat ditulis ulang seperti ini:

void examine_data(const struct blob data)
{
    ... use data.tr and data.len ...
}

struct blob = { .ptr = ..., .len = ... };
examine_data(blob);

struct blob get_data(void);
{
    ...
    return (struct blob){ .ptr = ...data..., .len = ...len... };
}
struct blob data = get_data();

Untuk beberapa alasan, saya pikir sebagian besar orang secara instingtif akan membuat exam_data mengambil pointer ke blob struct, tapi saya tidak mengerti mengapa. Itu masih mendapat pointer dan integer, itu hanya jauh lebih jelas bahwa mereka pergi bersama. Dan dalam kasus get_data tidak mungkin untuk mengacaukan dengan cara yang saya jelaskan sebelumnya, karena tidak ada nilai input untuk panjangnya, dan harus ada panjang yang dikembalikan.

dkagedal
sumber
Untuk apa nilainya, void examine data(const struct blob)itu tidak benar.
Chris Lutz
Terima kasih, ubah untuk memasukkan nama variabel.
dkagedal
1
"Tidak ada cara untuk mengatakan dari atas bahwa fungsi get_data tidak diizinkan untuk melihat apa yang ditunjukkan oleh len. Dan tidak ada yang membuat kompiler memeriksa apakah suatu nilai benar-benar dikembalikan melalui pointer itu." - ini sama sekali tidak masuk akal bagi saya (mungkin karena contoh Anda adalah kode yang tidak valid karena dua baris terakhir muncul di luar fungsi); tolong bisa jelaskan?
Adam Spires
2
Dua baris di bawah fungsi ada untuk menggambarkan bagaimana fungsi dipanggil. Fungsi tanda tangan tidak memberikan petunjuk pada fakta bahwa implementasi seharusnya hanya akan menulis ke pointer. Dan kompiler tidak memiliki cara untuk mengetahui bahwa ia harus memverifikasi bahwa nilai ditulis ke pointer, sehingga mekanisme nilai pengembalian hanya dapat dijelaskan dalam dokumentasi.
dkagedal
1
Alasan utama orang tidak melakukan ini lebih sering di C adalah historis. Sebelum ke C89, Anda tidak bisa melewatkan atau mengembalikan struct dengan nilai, jadi semua antarmuka sistem yang ada sebelum C89 dan secara logis harus melakukannya (seperti gettimeofday) menggunakan pointer, dan orang-orang mengambilnya sebagai contoh.
zwol

Jawaban:

202

Untuk struct kecil (misalnya point, rect) yang melewati nilai sangat bisa diterima. Namun, terlepas dari kecepatan, ada satu alasan lain mengapa Anda harus berhati-hati melewati / mengembalikan struct besar berdasarkan nilai: Stack space.

Banyak pemrograman C adalah untuk sistem tertanam, di mana memori adalah pada premium, dan ukuran tumpukan dapat diukur dalam KB atau bahkan Bytes ... Jika Anda melewati atau mengembalikan struct dengan nilai, salinan struct tersebut akan ditempatkan pada tumpukan, berpotensi menyebabkan situasi bahwa situs ini dinamai setelah ...

Jika saya melihat aplikasi yang tampaknya memiliki penggunaan tumpukan berlebihan, struct yang diteruskan oleh nilai adalah salah satu hal yang saya cari terlebih dahulu.

Roddy
sumber
2
"Jika Anda melewati atau mengembalikan struct dengan nilai, salinan struct tersebut akan ditempatkan pada stack" Saya akan memanggil braindead toolchain yang melakukannya. Ya, sangat menyedihkan bahwa begitu banyak yang akan melakukannya, tetapi bukan apa pun yang diminta oleh standar C. Kompiler yang waras akan mengoptimalkan semuanya.
Pasang kembali Monica
1
@KubaOber Inilah sebabnya mengapa hal itu tidak sering dilakukan: stackoverflow.com/questions/552134/…
Roddy
1
Apakah ada garis definitif yang memisahkan struct kecil dari struct besar?
Josie Thompson
63

Salah satu alasan untuk tidak melakukan ini yang belum disebutkan adalah bahwa ini dapat menyebabkan masalah kompatibilitas biner.

Bergantung pada kompiler yang digunakan, struktur dapat dilewatkan melalui tumpukan atau register tergantung pada opsi / implementasi kompiler

Lihat: http://gcc.gnu.org/onlinedocs/gcc/Code-Gen-Options.html

-fpcc-struct-return

-regreg-struct-return

Jika dua penyusun tidak setuju, hal-hal dapat meledak. Tak perlu dikatakan alasan utama untuk tidak melakukan ini diilustrasikan adalah konsumsi tumpukan dan alasan kinerja.

tonylo
sumber
4
Ini adalah jawaban yang saya cari.
dkagedal
2
Benar, tetapi opsi itu tidak terkait dengan nilai per nilai. mereka berhubungan dengan mengembalikan struct yang merupakan hal yang berbeda sama sekali. Mengembalikan barang dengan referensi biasanya merupakan cara yang pasti untuk menembak diri sendiri. int &bar() { int f; int &j(f); return j;};
Roddy
19

Untuk benar-benar menjawab pertanyaan ini, orang perlu menggali lebih dalam ke tanah pertemuan:

(Contoh berikut menggunakan gcc pada x86_64. Siapa pun boleh menambahkan arsitektur lain seperti MSVC, ARM, dll.)

Mari kita punya contoh program kami:

// foo.c

typedef struct
{
    double x, y;
} point;

void give_two_doubles(double * x, double * y)
{
    *x = 1.0;
    *y = 2.0;
}

point give_point()
{
    point a = {1.0, 2.0};
    return a;
}

int main()
{
    return 0;
}

Kompilasi dengan optimisasi penuh

gcc -Wall -O3 foo.c -o foo

Lihatlah majelis:

objdump -d foo | vim -

Inilah yang kami dapatkan:

0000000000400480 <give_two_doubles>:
    400480: 48 ba 00 00 00 00 00    mov    $0x3ff0000000000000,%rdx
    400487: 00 f0 3f 
    40048a: 48 b8 00 00 00 00 00    mov    $0x4000000000000000,%rax
    400491: 00 00 40 
    400494: 48 89 17                mov    %rdx,(%rdi)
    400497: 48 89 06                mov    %rax,(%rsi)
    40049a: c3                      retq   
    40049b: 0f 1f 44 00 00          nopl   0x0(%rax,%rax,1)

00000000004004a0 <give_point>:
    4004a0: 66 0f 28 05 28 01 00    movapd 0x128(%rip),%xmm0
    4004a7: 00 
    4004a8: 66 0f 29 44 24 e8       movapd %xmm0,-0x18(%rsp)
    4004ae: f2 0f 10 05 12 01 00    movsd  0x112(%rip),%xmm0
    4004b5: 00 
    4004b6: f2 0f 10 4c 24 f0       movsd  -0x10(%rsp),%xmm1
    4004bc: c3                      retq   
    4004bd: 0f 1f 00                nopl   (%rax)

Tidak termasuk noplbantalan, give_two_doubles()memiliki 27 byte sementara give_point()memiliki 29 byte. Di sisi lain, give_point()menghasilkan satu instruksi lebih sedikit daripadagive_two_doubles()

Yang menarik adalah kami memperhatikan bahwa kompiler telah dapat mengoptimalkan movke varian SSE2 yang lebih cepat movapddan movsd. Lebih jauh lagi, give_two_doubles()sebenarnya memindahkan data masuk dan keluar dari memori, yang membuat segalanya menjadi lambat.

Tampaknya banyak dari ini mungkin tidak berlaku di lingkungan tertanam (yang merupakan tempat bermain untuk C sebagian besar waktu saat ini). Saya bukan penyihir perakitan sehingga komentar apa pun akan diterima!

kizzx2
sumber
6
Menghitung jumlah instruksi tidak terlalu menarik, kecuali jika Anda dapat menunjukkan perbedaan besar, atau menghitung aspek yang lebih menarik seperti jumlah lompatan yang sulit diprediksi, dll. Properti kinerja aktual jauh lebih halus daripada penghitungan instruksi .
dkagedal
6
@Gambar: Benar. Dalam retrospeksi, saya pikir jawaban saya sendiri ditulis dengan sangat buruk. Meskipun saya tidak terlalu fokus pada jumlah instruksi (tidak tahu apa yang memberi Anda kesan: P), poin sebenarnya yang harus dibuat adalah bahwa passing struct dengan nilai lebih baik daripada meneruskan dengan referensi untuk tipe kecil. Bagaimanapun, memberikan nilai lebih disukai karena lebih sederhana (tidak ada juggling seumur hidup, tidak perlu khawatir tentang seseorang yang mengubah data Anda atau constsepanjang waktu) dan saya menemukan tidak ada banyak penalti kinerja (jika tidak mendapatkan) dalam penyalinan nilai pass-by-value , bertentangan dengan apa yang banyak orang percaya.
kizzx2
15

Solusi sederhana akan mengembalikan kode kesalahan sebagai nilai kembali dan segala sesuatu lainnya sebagai parameter dalam fungsi,
Parameter ini tentu saja merupakan struct, tetapi tidak melihat keuntungan tertentu yang melewati nilai ini, hanya mengirim pointer.
Melewati struktur berdasarkan nilai berbahaya, Anda harus sangat berhati-hati apa yang Anda lewati, ingat tidak ada copy constructor di C, jika salah satu parameter struktur adalah pointer, nilai pointer akan disalin, mungkin akan sangat membingungkan dan sulit untuk mempertahankan.

Hanya untuk menyelesaikan jawaban (kredit penuh untuk Roddy ) penggunaan stack adalah alasan lain tidak lulus struktur berdasarkan nilai, percayalah men-debug stack overflow adalah PITA nyata.

Putar ulang untuk berkomentar:

Melewati struct oleh pointer yang berarti bahwa beberapa entitas memiliki kepemilikan pada objek ini dan memiliki pengetahuan penuh tentang apa dan kapan harus dirilis. Melewati struct dengan nilai membuat referensi tersembunyi ke data internal struct (pointer ke struktur lain dll.) Di ini sulit untuk dipertahankan (mungkin tapi mengapa?).

Ilya
sumber
6
Tetapi melewatkan pointer tidak lebih "berbahaya" hanya karena Anda memasukkannya ke dalam struct, jadi saya tidak membelinya.
dkagedal
Poin bagus dalam menyalin struktur yang berisi pointer. Poin ini mungkin tidak terlalu jelas. Bagi mereka yang tidak tahu apa yang dia maksudkan, lakukan pencarian pada deep copy vs copy dangkal.
zooropa
1
Salah satu konvensi fungsi C adalah membuat parameter output didaftar terlebih dahulu sebelum parameter input, misalnya int func (char * out, char * in);
zooropa
Maksud Anda seperti bagaimana getaddrinfo () menempatkan parameter output sebagai yang terakhir? :-) Ada seribu set konvensi, dan Anda dapat memilih mana yang Anda inginkan.
dkagedal
10

Satu hal yang orang-orang di sini lupa sebutkan sejauh ini (atau saya mengabaikannya) adalah bahwa struct biasanya memiliki padding!

struct {
  short a;
  char b;
  short c;
  char d;
}

Setiap char adalah 1 byte, setiap short adalah 2 byte. Seberapa besar struct? Tidak, ini bukan 6 byte. Setidaknya tidak pada sistem yang lebih umum digunakan. Pada kebanyakan sistem akan menjadi 8. Masalahnya adalah, pelurusan tidak konstan, tergantung pada sistem, sehingga struct yang sama akan memiliki perataan yang berbeda dan ukuran yang berbeda pada sistem yang berbeda.

Tidak hanya itu padding akan semakin memakan tumpukan Anda, itu juga menambahkan ketidakpastian tidak dapat memprediksi padding di muka, kecuali jika Anda tahu bagaimana sistem Anda bantalan dan kemudian melihat setiap struct yang Anda miliki di aplikasi Anda dan menghitung ukurannya. untuk itu. Melewati sebuah pointer membutuhkan ruang yang dapat diprediksi - tidak ada ketidakpastian. Ukuran pointer dikenal untuk sistem, selalu sama, terlepas dari apa yang terlihat seperti struct dan ukuran pointer selalu dipilih dengan cara yang disejajarkan dan tidak perlu bantalan.

Mecki
sumber
2
Ya, tetapi padding ada tanpa ketergantungan melewati struktur dengan nilai atau dengan referensi.
Ilya
2
@dkagedal: Bagian mana dari "ukuran berbeda pada sistem berbeda" yang tidak Anda mengerti? Hanya karena memang begitu pada sistem Anda, Anda menganggap itu harus sama untuk yang lain - itulah sebabnya Anda tidak boleh melewati nilai. Sampel diubah sehingga gagal pada sistem Anda juga.
Mecki
2
Saya pikir komentar Mecki tentang struct padding relevan terutama untuk sistem embedded di mana ukuran stack mungkin menjadi masalah.
zooropa
1
Saya kira sisi lain dari argumen adalah bahwa jika struct Anda adalah struct sederhana (berisi beberapa jenis primitif), melewati nilai akan memungkinkan kompiler untuk menyulapnya menggunakan register - sedangkan jika Anda menggunakan pointer, hal-hal berakhir pada memori, yang lebih lambat. Itu mendapatkan level yang cukup rendah dan sangat tergantung pada arsitektur target Anda, jika ada informasi penting ini.
kizzx2
1
Kecuali jika struct Anda kecil atau CPU Anda memiliki banyak register (dan Intel CPU belum), data berakhir di stack dan itu juga memori dan secepat / lambat seperti memori lainnya. Pointer di sisi lain selalu kecil dan hanya sebuah pointer dan pointer itu sendiri biasanya akan selalu berakhir di register ketika digunakan lebih sering.
Mecki
9

Saya pikir pertanyaan Anda telah merangkum semuanya dengan cukup baik.

Satu keuntungan lain dari melewatkan struct oleh nilai adalah bahwa kepemilikan memori eksplisit. Tidak ada yang bertanya-tanya tentang apakah struct berasal dari heap, dan siapa yang memiliki tanggung jawab untuk membebaskannya.

Darron
sumber
9

Saya akan mengatakan melewati (tidak terlalu besar) struct dengan nilai, baik sebagai parameter dan sebagai nilai pengembalian, adalah teknik yang sangat sah. Seseorang harus berhati-hati, tentu saja, bahwa struct adalah jenis POD, atau semantik salinan ditentukan dengan baik.

Pembaruan: Maaf, saya memakai topi berpikir C ++. Saya ingat saat ketika tidak sah dalam C untuk mengembalikan struct dari suatu fungsi, tetapi ini mungkin telah berubah sejak saat itu. Saya masih akan mengatakan itu valid selama semua kompiler yang Anda harapkan mendukung praktik ini.

Greg Hewgill
sumber
Perhatikan bahwa pertanyaan saya adalah tentang C, bukan C ++.
dkagedal
Ini valid untuk mengembalikan struct dari fungsi tidak berguna :)
Ilya
1
Saya suka saran llya untuk menggunakan pengembalian sebagai kode kesalahan dan parameter untuk mengembalikan data dari fungsi.
zooropa
8

Ini adalah sesuatu yang tidak disebutkan siapa pun:

void examine_data(const char *c, size_t l)
{
    c[0] = 'l'; // compiler error
}

void examine_data(const struct blob blob)
{
    blob.ptr[0] = 'l'; // perfectly legal, quite likely to blow up at runtime
}

Anggota dari const structyang const, tapi kalau itu anggota adalah pointer (seperti char *), menjadi char *constdaripada const char *kita benar-benar inginkan. Tentu saja, kita dapat berasumsi bahwa constini adalah dokumentasi dari niat, dan bahwa siapa pun yang melanggar ini sedang menulis kode yang buruk, tetapi itu tidak cukup baik bagi sebagian orang (terutama mereka yang hanya menghabiskan waktu empat jam mencari penyebab jatuh).

Alternatifnya mungkin untuk membuat struct const_blob { const char *c; size_t l }dan menggunakannya, tapi itu agak berantakan - itu masuk ke masalah skema penamaan yang sama yang saya miliki dengan typedefing pointer. Dengan demikian, kebanyakan orang tetap hanya memiliki dua parameter (atau, lebih mungkin untuk kasus ini, menggunakan pustaka string).

Chris Lutz
sumber
Ya itu sangat legal, dan juga sesuatu yang ingin Anda lakukan kadang-kadang. Tapi saya setuju bahwa itu adalah batasan dari solusi struct bahwa Anda tidak dapat membuat pointer mereka menunjuk ke titik ke const.
dkagedal
Sebuah gotcha jahat dengan struct const_blobsolusinya adalah bahwa bahkan jika const_blobmemiliki anggota yang berbeda dari blobhanya dalam "keteguhan tidak langsung", tipe struct blob*ke a struct const_blob*akan dianggap berbeda untuk keperluan aturan alias yang ketat. Akibatnya, jika kode melemparkan blob*ke a const_blob*, setiap penulisan berikutnya ke struktur yang mendasari menggunakan satu jenis akan secara diam-diam membatalkan petunjuk yang ada dari jenis lain, sehingga setiap penggunaan akan memanggil Perilaku Tidak Terdefinisi (yang biasanya tidak berbahaya, tetapi bisa mematikan) .
supercat
5

Halaman 150 dari Tutorial Perakitan PC di http://www.drpaulcarter.com/pcasm/ memiliki penjelasan yang jelas tentang bagaimana C memungkinkan suatu fungsi mengembalikan sebuah struct:

C juga memungkinkan tipe struktur untuk digunakan sebagai nilai balik suatu fungsi. Jelas struktur tidak dapat dikembalikan dalam register EAX. Kompiler yang berbeda menangani situasi ini secara berbeda. Solusi umum yang digunakan kompiler adalah menulis ulang fungsi secara internal sebagai salah satu yang menggunakan pointer struktur sebagai parameter. Pointer digunakan untuk meletakkan nilai balik ke dalam struktur yang ditentukan di luar rutin yang disebut.

Saya menggunakan kode C berikut untuk memverifikasi pernyataan di atas:

struct person {
    int no;
    int age;
};

struct person create() {
    struct person jingguo = { .no = 1, .age = 2};
    return jingguo;
}

int main(int argc, const char *argv[]) {
    struct person result;
    result = create();
    return 0;
}

Gunakan "gcc -S" untuk menghasilkan perakitan untuk bagian kode C ini:

    .file   "foo.c"
    .text
.globl create
    .type   create, @function
create:
    pushl   %ebp
    movl    %esp, %ebp
    subl    $16, %esp
    movl    8(%ebp), %ecx
    movl    $1, -8(%ebp)
    movl    $2, -4(%ebp)
    movl    -8(%ebp), %eax
    movl    -4(%ebp), %edx
    movl    %eax, (%ecx)
    movl    %edx, 4(%ecx)
    movl    %ecx, %eax
    leave
    ret $4
    .size   create, .-create
.globl main
    .type   main, @function
main:
    pushl   %ebp
    movl    %esp, %ebp
    subl    $20, %esp
    leal    -8(%ebp), %eax
    movl    %eax, (%esp)
    call    create
    subl    $4, %esp
    movl    $0, %eax
    leave
    ret
    .size   main, .-main
    .ident  "GCC: (Ubuntu 4.4.3-4ubuntu5) 4.4.3"
    .section    .note.GNU-stack,"",@progbits

Tumpukan sebelum panggilan dibuat:

        +---------------------------+
ebp     | saved ebp                 |
        +---------------------------+
ebp-4   | age part of struct person | 
        +---------------------------+
ebp-8   | no part of struct person  |
        +---------------------------+        
ebp-12  |                           |
        +---------------------------+
ebp-16  |                           |
        +---------------------------+
ebp-20  | ebp-8 (address)           |
        +---------------------------+

Tumpukan tepat setelah memanggil buat:

        +---------------------------+
        | ebp-8 (address)           |
        +---------------------------+
        | return address            |
        +---------------------------+
ebp,esp | saved ebp                 |
        +---------------------------+
Jingguo Yao
sumber
2
ada dua masalah disini. Yang paling jelas adalah bahwa ini sama sekali tidak menggambarkan "bagaimana C memungkinkan suatu fungsi mengembalikan sebuah struct". Ini hanya menjelaskan bagaimana hal itu dapat dilakukan pada perangkat keras x86 32-bit, yang kebetulan menjadi salah satu arsitektur paling terbatas ketika Anda melihat jumlah register dll. Masalah kedua adalah cara kompiler C menghasilkan kode untuk mengembalikan nilai. ditentukan oleh ABI (kecuali untuk fungsi yang tidak diekspor atau inline). Dan omong-omong, fungsi-fungsi inline mungkin adalah salah satu tempat di mana mengembalikan struct paling berguna.
dkagedal
Terima kasih atas koreksinya. Untuk rincian lengkap tentang konvensi panggilan, en.wikipedia.org/wiki/Calling_convention adalah referensi yang bagus.
Jingguo Yao
@dkagedal: Yang penting bukan hanya bahwa x86 kebetulan melakukan hal-hal seperti ini, tetapi bahwa ada pendekatan "universal" (yaitu yang ini) yang akan memungkinkan kompiler untuk platform apa pun untuk mendukung pengembalian dari setiap jenis struktur yang tidak t begitu besar untuk meniup tumpukan. Sementara kompiler untuk banyak platform akan menggunakan cara lain yang lebih efisien untuk menangani beberapa nilai kembali tipe struktur, tidak perlu bahasa untuk membatasi tipe pengembalian struktur agar platform dapat menangani secara optimal.
supercat
0

Saya hanya ingin menunjukkan satu keuntungan melewati struct Anda dengan nilai adalah bahwa kompiler pengoptimalisasi mungkin lebih baik mengoptimalkan kode Anda.

Vad
sumber