C# Mengembalikan pointer yang dibuat dengan stackalloc di dalam suatu fungsi

Saya memiliki kode C# yang berinteraksi dengan kode C++, yang melakukan operasi dengan string.

Saya memiliki potongan kode ini di kelas pembantu statis:

internal static unsafe byte* GetConstNullTerminated(string text, Encoding encoding)
{
    int charCount = text.Length;
    fixed (char* chars = text)
    {
        int byteCount = encoding.GetByteCount(chars, charCount);
        byte* bytes = stackalloc byte[byteCount + 1];
        encoding.GetBytes(chars, charCount, bytes, byteCount);
        *(bytes + byteCount) = 0;
        return bytes;
    }
}

Seperti yang Anda lihat, ia mengembalikan pointer ke byte yang dibuat dengan kata kunci stackalloc.
Namun dari Spesifikasi C# 18.8:

Semua blok memori yang dialokasikan tumpukan yang dibuat selama eksekusi anggota fungsi secara otomatis dibuang ketika anggota fungsi tersebut kembali.

Apakah ini berarti penunjuk sebenarnya tidak valid segera setelah metode kembali?

Penggunaan metode saat ini:

byte* bytes = StringHelper.GetConstNullTerminated(value ?? string.Empty, Encoding);
DirectFunction(NativeMethods.SCI_SETTEXT, UIntPtr.Zero, (IntPtr) bytes);

Haruskah kodenya diubah menjadi

...
int byteCount = encoding.GetByteCount(chars, charCount);
byte[] byteArray = new byte[byteCount + 1];
fixed (byte* bytes = byteArray)
{
    encoding.GetBytes(chars, charCount, bytes, byteCount);
    *(bytes + byteCount) = 0;
}
return byteArray;

Dan gunakan fixed lagi pada array yang dikembalikan, untuk meneruskan pointer ke metode DirectFunction?

Saya mencoba meminimalkan jumlah penggunaan fixed (termasuk pernyataan fixed dalam kelebihan beban lainnya pada GetByteCount() dan GetBytes() dari Encoding).

tl;dr

  1. Apakah penunjuknya tidak valid segera setelah metode kembali? Apakah tidak valid saat diteruskan ke DirectFunction()?

  2. Jika demikian, apa cara terbaik untuk menggunakan pernyataan fixed paling sedikit untuk mencapai tugas tersebut?


person A. A. Ron    schedule 06.05.2017    source sumber


Jawaban (2)


Apakah ini berarti penunjuk sebenarnya tidak valid segera setelah metode kembali?

Ya, secara teknis tidak valid - meskipun hampir pasti tidak akan terdeteksi. Skenario ini disebabkan oleh diri sendiri melalui unsafe. Tindakan apa pun pada memori tersebut sekarang memiliki perilaku tidak terdefinisi. Apa pun yang Anda lakukan, kecuali metode pemanggilan tertentu, mungkin secara acak menimpa memori tersebut - atau tidak - bergantung pada ukuran dan kedalaman bingkai tumpukan relatif.

Skenario ini secara khusus adalah salah satu skenario yang diharapkan dapat ditargetkan oleh perubahan ref di masa depan, artinya: mengizinkan stackalloc menjadi ref (bukan penunjuk), dengan kompiler mengetahui bahwa itu adalah tipe referensi tumpukan ref atau seperti ref, dan sehingga melarang ref-pengembalian nilai itu.

Pada akhirnya, saat Anda mengetik unsafe Anda mengatakan "Saya bertanggung jawab penuh jika terjadi kesalahan". Dalam hal ini memang salah.


adalah valid untuk menggunakan pointer sebelum meninggalkan metode, jadi salah satu pendekatan yang mungkin dilakukan adalah (dengan asumsi Anda menginginkan API dengan tujuan yang cukup umum) untuk memungkinkan pemanggil meneruskan a delegasi atau antarmuka yang menentukan apa yang pemanggil ingin Anda lakukan dengan penunjuk, mis.

StringHelper.GetConstNullTerminated(value ?? string.Empty, Encoding,
    ptr => DirectFunction(NativeMethods.SCI_SETTEXT, UIntPtr.Zero, (IntPtr) ptr));

dengan:

unsafe delegate void PointerAction(byte* ptr);
internal static unsafe void GetConstNullTerminated(string text, Encoding encoding,
    PointerAction action)
{
    int charCount = text.Length;
    fixed (char* chars = text)
    {
        int byteCount = encoding.GetByteCount(chars, charCount);
        byte* bytes = stackalloc byte[byteCount + 1];
        encoding.GetBytes(chars, charCount, bytes, byteCount);
        *(bytes + byteCount) = 0;
        action(bytes);
    }
}

Perhatikan juga bahwa string yang sangat besar dapat menyebabkan Anda mengalami stack-overflow.

person Marc Gravell    schedule 06.05.2017
comment
Tidak pernah berpikir untuk meloloskan delegasi. Solusi ini akan secepat copy-paste-inlining GetConstNullTerminated secara manual ke tujuan, jika tidak ada penurunan kinerja dalam menggunakan delegasi. Saya harus melakukan penelitian tentang kinerja delegasi sekarang... - person A. A. Ron; 06.05.2017
comment
@ A.A.Ron kinerja delegasi akan benar-benar beragam dibandingkan dengan pekerjaan pengkodean dan berjalan dua kali dari string input (sekali untuk panjangnya, sekali untuk penyandian) - person Marc Gravell; 06.05.2017
comment
Akankah delegasi benar-benar meningkatkan kinerja dibandingkan hanya mengalokasikan pada heap? Delegasi masih memerlukan alokasi heap. Bergantung pada ukuran string yang diteruskan, Anda dapat dengan mudah mengalokasikan lebih banyak memori untuk menampung delegasi daripada string sebenarnya. - person Jarra McIntyre; 07.05.2017
comment
@Jarra lambda yang ditampilkan tidak menangkap variabel apa pun, dan karena itu sebenarnya didukung oleh bidang statis yang menyimpan instance delegasi yang digunakan kembali, sehingga tidak ada biaya alokasi berkelanjutan setelah panggilan pertama. Dalam kasus yang lebih umum, instance delegasi tidak besar - secara konseptual hanya instance target dan penunjuk metode - pada dasarnya 16 byte pada x64 (ditambah header objek, ditambah beberapa overhead lainnya) - tidak mahal, tapi tentu saja nol alokasi (sebagai ditampilkan) lebih murah. - person Marc Gravell; 07.05.2017

stackalloc menyebabkan memori dialokasikan pada tumpukan. Tumpukan secara otomatis dibatalkan ketika fungsi kembali. C# melindungi Anda dari membuat penunjuk gantung dengan tidak mengizinkan Anda mengembalikan penunjuk, karena tidak mungkin memori masih valid setelah tumpukan dilepas saat fungsi kembali.

Jika Anda ingin memori berada di luar cakupan fungsi yang mengalokasikannya, Anda tidak dapat mengalokasikannya di tumpukan. Anda harus mengalokasikan di heap melalui new.

person Jarra McIntyre    schedule 06.05.2017
comment
C# melindungi Anda dari membuat pointer gantung dengan tidak membiarkan Anda mengembalikan pointer C# tidak mengeluh sama sekali, dan kode dapat dikompilasi tanpa masalah. - person A. A. Ron; 06.05.2017
comment
Permintaan maaf saya. Saya pikir C# memiliki beberapa analisis pelarian dasar untuk menangkap kasus seperti ini. - person Jarra McIntyre; 07.05.2017