Mengapa tipe Tuple baru di .Net 4.0 adalah tipe referensi (kelas) dan bukan tipe nilai (struct)

89

Adakah yang tahu jawabannya dan / atau punya pendapat tentang ini?

Karena tupel biasanya tidak terlalu besar, saya akan berasumsi akan lebih masuk akal untuk menggunakan struct daripada kelas untuk ini. Apa yang kamu katakan?

Bent Rasmussen
sumber
1
Untuk siapa pun yang tersandung di sini setelah 2016. Dalam c # 7 dan yang lebih baru, literal Tuple termasuk dalam keluarga tipe ValueTuple<...>. Lihat referensi di C # jenis tupel
Tamir Daniely

Jawaban:

94

Microsoft membuat semua jenis referensi jenis tupel untuk kepentingan kesederhanaan.

Saya pribadi berpikir ini adalah kesalahan. Tupel dengan lebih dari 4 bidang sangat tidak biasa dan harus diganti dengan alternatif yang lebih bertipe (seperti jenis rekaman di F #) sehingga hanya tupel kecil yang menarik. Tolok ukur saya sendiri menunjukkan bahwa tupel tanpa kotak hingga 512 byte masih bisa lebih cepat daripada tupel kotak.

Meskipun efisiensi memori adalah salah satu perhatian, saya yakin masalah dominan adalah overhead pengumpul sampah .NET. Alokasi dan pengumpulan sangat mahal di .NET karena pengumpul sampahnya belum dioptimalkan secara maksimal (misalnya dibandingkan dengan JVM). Selain itu, default .NET GC (workstation) belum diparalelkan. Akibatnya, program paralel yang menggunakan tupel menggiling menjadi berhenti karena semua inti bersaing untuk pengumpul sampah bersama, menghancurkan skalabilitas. Ini bukan hanya perhatian yang dominan tetapi, AFAIK, sama sekali diabaikan oleh Microsoft ketika mereka memeriksa masalah ini.

Kekhawatiran lainnya adalah pengiriman virtual. Jenis referensi mendukung subtipe dan, oleh karena itu, anggotanya biasanya dipanggil melalui pengiriman virtual. Sebaliknya, tipe nilai tidak dapat mendukung subtipe sehingga pemanggilan anggota sepenuhnya tidak ambigu dan selalu dapat dilakukan sebagai panggilan fungsi langsung. Pengiriman virtual sangat mahal pada perangkat keras modern karena CPU tidak dapat memprediksi di mana penghitung program akan berakhir. JVM berusaha keras untuk mengoptimalkan pengiriman virtual tetapi .NET tidak. Namun, .NET menyediakan jalan keluar dari pengiriman virtual dalam bentuk tipe nilai. Jadi merepresentasikan tupel sebagai tipe nilai, sekali lagi, dapat meningkatkan kinerja secara dramatis di sini. Misalnya, meneleponGetHashCode pada 2-tupel satu juta kali membutuhkan 0,17 tetapi memanggilnya pada struct yang setara hanya membutuhkan 0,008s, yaitu tipe nilai 20x lebih cepat dari tipe referensi.

Situasi nyata di mana masalah kinerja dengan tupel biasanya muncul adalah dalam penggunaan tupel sebagai kunci dalam kamus. Saya benar-benar menemukan utas ini dengan mengikuti tautan dari pertanyaan Stack Overflow F # menjalankan algoritme saya lebih lambat dari Python! di mana program F # penulis ternyata lebih lambat dari Python-nya justru karena dia menggunakan tupel kotak. Unboxing secara manual menggunakan tipe tulisan tangan structmembuat program F #-nya beberapa kali lebih cepat, dan lebih cepat dari Python. Masalah ini tidak akan pernah muncul jika tupel diwakili oleh tipe nilai dan bukan tipe referensi untuk memulai ...

JD
sumber
2
@Bent: Ya, itulah yang saya lakukan ketika saya menemukan tupel di jalur panas di F #. Akan lebih baik jika mereka menyediakan tupel dalam kotak dan tidak berkotak di .NET Framework meskipun ...
JD
18
Mengenai pengiriman virtual, menurut saya kesalahan Anda salah: Tuple<_,...,_>jenisnya bisa saja disegel, dalam hal ini tidak ada pengiriman virtual yang diperlukan meskipun merupakan jenis referensi. Saya lebih penasaran tentang mengapa mereka tidak disegel daripada mengapa mereka adalah tipe referensi.
kvb
2
Dari pengujian saya, untuk skenario di mana tupel akan dihasilkan dalam satu fungsi dan dikembalikan ke fungsi lain, dan kemudian tidak pernah digunakan lagi, struktur bidang terbuka tampaknya menawarkan kinerja yang unggul untuk item data ukuran apa pun yang tidak terlalu besar untuk meledak tumpukan. Kelas yang tidak dapat diubah hanya lebih baik jika referensi akan diedarkan cukup untuk membenarkan biaya konstruksi mereka (semakin besar item data, semakin sedikit mereka harus diedarkan agar tradeoff menguntungkan mereka). Karena tupel seharusnya hanya mewakili sekumpulan variabel yang saling menempel, sebuah struct akan tampak ideal.
supercat
2
"tupel tanpa kotak hingga 512 byte masih bisa lebih cepat daripada kotak" - skenario yang mana? Anda mungkin dapat mengalokasikan struct 512B lebih cepat daripada instance kelas yang menyimpan data 512B, tetapi meneruskannya akan lebih dari 100 kali lebih lambat (anggap x86). Apakah ada sesuatu yang saya abaikan?
Groo
45

Alasannya kemungkinan besar karena hanya tupel yang lebih kecil yang masuk akal sebagai tipe nilai karena mereka memiliki jejak memori yang kecil. Tupel yang lebih besar (yaitu yang memiliki lebih banyak properti) sebenarnya akan mengalami penurunan performa karena ukurannya lebih besar dari 16 byte.

Daripada memiliki beberapa tuple menjadi tipe nilai dan yang lain menjadi tipe referensi dan memaksa pengembang untuk mengetahui mana yang menurut saya akan dibayangkan oleh orang-orang di Microsoft bahwa membuat mereka semua tipe referensi lebih sederhana.

Ah, kecurigaan dikonfirmasi! Silakan lihat Building Tuple :

Keputusan besar pertama adalah apakah akan memperlakukan tupel baik sebagai referensi atau tipe nilai. Karena mereka tidak dapat diubah setiap kali Anda ingin mengubah nilai tupel, Anda harus membuat yang baru. Jika mereka adalah tipe referensi, ini berarti ada banyak sampah yang dihasilkan jika Anda mengubah elemen dalam tupel dalam loop yang ketat. F # tupel adalah tipe referensi, tetapi ada perasaan dari tim bahwa mereka dapat mewujudkan peningkatan kinerja jika dua, dan mungkin tiga, elemen tupel adalah tipe nilai. Beberapa tim yang telah membuat tupel internal menggunakan nilai alih-alih tipe referensi, karena skenario mereka sangat sensitif untuk membuat banyak objek terkelola. Mereka menemukan bahwa menggunakan tipe nilai memberi mereka kinerja yang lebih baik. Dalam draf pertama spesifikasi tupel kami, kami mempertahankan tupel dua, tiga, dan empat elemen sebagai tipe nilai, dengan sisanya menjadi tipe referensi. Namun, selama pertemuan desain yang menyertakan perwakilan dari bahasa lain diputuskan bahwa desain "terpisah" ini akan membingungkan, karena semantik yang sedikit berbeda antara kedua jenis tersebut. Konsistensi dalam perilaku dan desain ditentukan untuk menjadi prioritas yang lebih tinggi daripada potensi peningkatan kinerja. Berdasarkan masukan ini, kami mengubah desain sehingga semua tupel adalah tipe referensi, meskipun kami meminta tim F # untuk melakukan investigasi kinerja untuk melihat apakah mengalami percepatan saat menggunakan tipe nilai untuk beberapa ukuran tupel. Itu cara yang baik untuk menguji ini, karena kompilernya, ditulis dalam F #, adalah contoh bagus dari program besar yang menggunakan tupel dalam berbagai skenario. Pada akhirnya, tim F # menemukan bahwa itu tidak mendapatkan peningkatan kinerja ketika beberapa tupel adalah tipe nilai dan bukan tipe referensi. Ini membuat kami merasa lebih baik tentang keputusan kami untuk menggunakan jenis referensi untuk tuple.

Andrew Hare
sumber
3
Diskusi hebat di sini: blogs.msdn.com/bclteam/archive/2009/07/07/…
Keith Adler
Ahh, begitu. Saya masih sedikit bingung bahwa tipe nilai tidak berarti apa-apa dalam praktiknya di sini: P
Bent Rasmussen
Saya baru saja membaca komentar tentang tidak ada antarmuka umum dan ketika melihat kode sebelumnya, itulah hal lain yang mengejutkan saya. Benar-benar tidak menginspirasi betapa ungeneriknya tipe Tuple. Tapi, saya rasa Anda selalu bisa membuatnya sendiri ... Lagipula, tidak ada dukungan sintaksis di C #. Namun setidaknya ... Meski begitu, penggunaan obat generik dan batasannya masih terasa terbatas di .Net. Ada potensi besar untuk pustaka abstrak yang sangat umum tetapi obat generik mungkin memerlukan hal-hal tambahan seperti tipe kembalian kovarian.
Bent Rasmussen
7
Batas "16 byte" Anda palsu. Ketika saya menguji ini di .NET 4 saya menemukan bahwa GC sangat lambat sehingga tupel yang tidak dikotak-kotak hingga 512 byte masih bisa lebih cepat. Saya juga mempertanyakan hasil benchmark Microsoft. Saya yakin mereka mengabaikan paralelisme (kompiler F # tidak paralel) dan di situlah menghindari GC benar-benar terbayar karena GC workstation NET juga tidak paralel.
JD
Karena penasaran, saya bertanya-tanya apakah tim compiler menguji ide membuat tuple menjadi struct EXPOSED-FIELD ? Jika seseorang memiliki instance dari tipe dengan berbagai sifat, dan membutuhkan instance yang identik kecuali untuk satu sifat yang berbeda, struct bidang terbuka dapat melakukannya jauh lebih cepat daripada jenis lainnya, dan keuntungan hanya tumbuh saat struct mendapatkan lebih besar.
supercat
7

Jika tipe .NET System.Tuple <...> didefinisikan sebagai struct, mereka tidak akan dapat diskalakan. Misalnya, tupel terner dari bilangan bulat panjang saat ini diskalakan sebagai berikut:

type Tuple3 = System.Tuple<int64, int64, int64>
type Tuple33 = System.Tuple<Tuple3, Tuple3, Tuple3>
sizeof<Tuple3> // Gets 4
sizeof<Tuple33> // Gets 4

Jika tupel terner didefinisikan sebagai struct, hasilnya adalah sebagai berikut (berdasarkan contoh uji yang saya implementasikan):

sizeof<Tuple3> // Would get 32
sizeof<Tuple33> // Would get 104

Karena tupel memiliki dukungan sintaks built-in dalam F #, dan mereka sangat sering digunakan dalam bahasa ini, tupel "struct" akan membuat programmer F # berisiko menulis program yang tidak efisien bahkan tanpa menyadarinya. Itu akan terjadi dengan sangat mudah:

let t3 = 1L, 2L, 3L
let t33 = t3, t3, t3

Menurut pendapat saya, tupel "struct" akan menyebabkan probabilitas yang tinggi untuk menciptakan inefisiensi yang signifikan dalam pemrograman sehari-hari. Di sisi lain, tuple "class" yang ada saat ini juga menyebabkan inefisiensi tertentu, seperti yang disebutkan oleh @Jon. Namun, saya berpikir bahwa produk dari "probabilitas kejadian" kali "potensi kerusakan" akan jauh lebih tinggi dengan struct daripada saat ini dengan kelas. Oleh karena itu, implementasi saat ini adalah kejahatan yang lebih rendah.

Idealnya, akan ada tupel "class" dan "struct", keduanya dengan dukungan sintaksis di F #!

Sunting (2017-10-07)

Struct tuple sekarang didukung penuh sebagai berikut:

Marc Sigrist
sumber
2
Jika seseorang menghindari penyalinan yang tidak perlu, struct kolom terbuka dengan ukuran berapa pun akan lebih efisien daripada kelas yang tidak dapat diubah dengan ukuran yang sama, kecuali setiap instance disalin cukup sering sehingga biaya penyalinan tersebut mengatasi biaya pembuatan objek heap ( jumlah impas salinan bervariasi dengan ukuran objek). Menyalin tersebut mungkin tidak dapat dihindari jika seseorang ingin struct yang berpura-pura menjadi berubah, tetapi struct yang dirancang untuk muncul sebagai koleksi variabel (yang adalah apa struct yang ) dapat digunakan secara efisien bahkan ketika mereka besar.
supercat
2
Mungkin F # tidak cocok dengan ide melewatkan struct ref, atau mungkin tidak menyukai fakta bahwa apa yang disebut "struct yang tidak dapat diubah" tidak, terutama ketika dimasukkan kotak. Sayang sekali .net tidak pernah mengimplementasikan konsep melewatkan parameter oleh sebuah penegakan const ref, karena dalam banyak kasus semantik seperti itu yang benar-benar dibutuhkan.
supercat
1
Kebetulan, saya menganggap biaya GC diamortisasi sebagai bagian dari biaya alokasi objek; jika L0 GC akan diperlukan setelah setiap megabyte alokasi, maka biaya alokasi 64 byte adalah sekitar 1 / 16.000 dari biaya L0 GC, ditambah sebagian kecil dari biaya L1 atau L2 GC yang diperlukan sebagai konsekuensi dari itu.
supercat
4
"Saya pikir produk dari kemungkinan terjadinya kali potensi kerusakan akan jauh lebih tinggi dengan struct daripada saat ini dengan kelas". FWIW, saya sangat jarang melihat tupel tupel di alam liar dan menganggapnya sebagai cacat desain, tetapi saya sangat sering melihat orang berjuang dengan kinerja yang buruk saat menggunakan (ref) tupel sebagai kunci di a Dictionary, misalnya di sini: stackoverflow.com/questions/5850243 /…
JD
3
@ Jon Sudah dua tahun sejak saya menulis jawaban ini, dan saya sekarang setuju dengan Anda bahwa akan lebih disukai jika setidaknya 2 dan 3-tupel adalah struct. Saran suara pengguna bahasa F # telah dibuat dalam hal ini. Masalah ini memiliki urgensi, karena telah terjadi pertumbuhan besar-besaran aplikasi dalam data besar, keuangan kuantitatif, dan permainan dalam beberapa tahun terakhir.
Marc Sigrist
4

Untuk 2-tupel, Anda masih dapat selalu menggunakan KeyValuePair <TKey, TValue> dari versi Common Type System sebelumnya. Ini adalah tipe nilai.

Klarifikasi kecil untuk artikel Matt Ellis adalah bahwa perbedaan penggunaan semantik antara referensi dan tipe nilai hanya "sedikit" ketika keabadian berlaku (yang, tentu saja, akan menjadi kasus di sini). Namun demikian, saya pikir akan lebih baik dalam desain BCL untuk tidak menimbulkan kebingungan karena Tuple menyeberang ke jenis referensi di ambang tertentu.

Glenn Slayden
sumber
Jika nilai akan digunakan satu kali setelah dikembalikan, struct kolom terbuka dengan ukuran berapa pun akan mengungguli jenis lainnya, asalkan nilainya tidak terlalu besar untuk meledakkan tumpukan. Biaya pembuatan objek kelas hanya akan diperoleh kembali jika referensi akhirnya dibagikan beberapa kali. Ada kalanya berguna untuk tipe heterogen ukuran tetap tujuan umum untuk menjadi kelas, tetapi ada saat lain ketika struct akan lebih baik - bahkan untuk hal-hal "besar".
supercat
Terima kasih telah menambahkan aturan praktis yang berguna ini. Namun saya berharap Anda tidak salah memahami posisi saya: Saya adalah pecandu tipe-nilai. ( stackoverflow.com/a/14277068 seharusnya tidak diragukan lagi).
Glenn Slayden
Jenis nilai adalah salah satu fitur hebat dari .net, tetapi sayangnya orang yang menulis msdn dox gagal mengenali bahwa ada beberapa kasus penggunaan terputus-putus untuk mereka, dan bahwa kasus penggunaan yang berbeda harus memiliki pedoman yang berbeda. Gaya struct msdn merekomendasikan hanya boleh digunakan dengan struct yang mewakili nilai homogen, tetapi jika seseorang perlu mewakili beberapa nilai independen yang diikat bersama dengan lakban, seseorang tidak boleh menggunakan yang gaya struct - salah satu harus menggunakan struct dengan bidang publik yang terbuka.
supercat
0

Saya tidak tahu tetapi jika Anda pernah menggunakan F # Tuple adalah bagian dari bahasa. Jika saya membuat .dll dan mengembalikan jenis Tuple, alangkah baiknya memiliki jenis untuk dimasukkan. Saya menduga sekarang F # adalah bagian dari bahasa (.Net 4) beberapa modifikasi CLR dibuat untuk mengakomodasi beberapa struktur umum di F #

Dari http://en.wikibooks.org/wiki/F_Sharp_Programming/Tuples_and_Records

let scalarMultiply (s : float) (a, b, c) = (a * s, b * s, c * s);;

val scalarMultiply : float -> float * float * float -> float * float * float

scalarMultiply 5.0 (6.0, 10.0, 20.0);;
val it : float * float * float = (30.0, 50.0, 100.0)
Bionic Cyborg
sumber