Apakah perlakuan kompilator terhadap variabel antarmuka implisit didokumentasikan?

86

Saya mengajukan pertanyaan serupa tentang variabel antarmuka implisit belum lama ini.

Sumber pertanyaan ini adalah bug dalam kode saya karena saya tidak menyadari keberadaan variabel antarmuka implisit yang dibuat oleh kompilator. Variabel ini diselesaikan ketika prosedur yang dimilikinya selesai. Ini pada gilirannya menyebabkan bug karena masa pakai variabel lebih lama dari yang saya perkirakan.

Sekarang, saya memiliki proyek sederhana untuk mengilustrasikan beberapa perilaku menarik dari compiler:

program ImplicitInterfaceLocals;

{$APPTYPE CONSOLE}

uses
  Classes;

function Create: IInterface;
begin
  Result := TInterfacedObject.Create;
end;

procedure StoreToLocal;
var
  I: IInterface;
begin
  I := Create;
end;

procedure StoreViaPointerToLocal;
var
  I: IInterface;
  P: ^IInterface;
begin
  P := @I;
  P^ := Create;
end;

begin
  StoreToLocal;
  StoreViaPointerToLocal;
end.

StoreToLocaldikompilasi seperti yang Anda bayangkan. Variabel lokal I, hasil fungsi, diteruskan sebagai varparameter implisit ke Create. Pembersihan untuk StoreToLocalhasil dalam satu panggilan ke IntfClear. Tidak ada kejutan di sana.

Namun, StoreViaPointerToLocaldiperlakukan berbeda. Kompilator membuat variabel lokal implisit yang diteruskannya Create. Saat Createkembali, tugas ke P^dilakukan. Ini meninggalkan rutinitas dengan dua variabel lokal yang memegang referensi ke antarmuka. Pembersihan akan StoreViaPointerToLocalmenghasilkan dua panggilan ke IntfClear.

Kode yang dikompilasi StoreViaPointerToLocaladalah seperti ini:

ImplicitInterfaceLocals.dpr.24: begin
00435C50 55               push ebp
00435C51 8BEC             mov ebp,esp
00435C53 6A00             push $00
00435C55 6A00             push $00
00435C57 6A00             push $00
00435C59 33C0             xor eax,eax
00435C5B 55               push ebp
00435C5C 689E5C4300       push $00435c9e
00435C61 64FF30           push dword ptr fs:[eax]
00435C64 648920           mov fs:[eax],esp
ImplicitInterfaceLocals.dpr.25: P := @I;
00435C67 8D45FC           lea eax,[ebp-$04]
00435C6A 8945F8           mov [ebp-$08],eax
ImplicitInterfaceLocals.dpr.26: P^ := Create;
00435C6D 8D45F4           lea eax,[ebp-$0c]
00435C70 E873FFFFFF       call Create
00435C75 8B55F4           mov edx,[ebp-$0c]
00435C78 8B45F8           mov eax,[ebp-$08]
00435C7B E81032FDFF       call @IntfCopy
ImplicitInterfaceLocals.dpr.27: end;
00435C80 33C0             xor eax,eax
00435C82 5A               pop edx
00435C83 59               pop ecx
00435C84 59               pop ecx
00435C85 648910           mov fs:[eax],edx
00435C88 68A55C4300       push $00435ca5
00435C8D 8D45F4           lea eax,[ebp-$0c]
00435C90 E8E331FDFF       call @IntfClear
00435C95 8D45FC           lea eax,[ebp-$04]
00435C98 E8DB31FDFF       call @IntfClear
00435C9D C3               ret 

Saya bisa menebak mengapa kompilator melakukan ini. Ketika dapat membuktikan bahwa menugaskan ke variabel hasil tidak akan menimbulkan pengecualian (yaitu jika variabel adalah lokal) maka ia menggunakan variabel hasil secara langsung. Jika tidak, ia menggunakan lokal implisit dan menyalin antarmuka setelah fungsi kembali sehingga memastikan bahwa kami tidak membocorkan referensi jika terjadi pengecualian.

Tetapi saya tidak dapat menemukan pernyataan apa pun tentang ini di dokumentasi. Itu penting karena umur antarmuka itu penting dan sebagai programmer Anda harus dapat memengaruhinya sesekali.

Jadi, apakah ada yang tahu jika ada dokumentasi tentang perilaku ini? Jika tidak, apakah ada yang punya pengetahuan lebih tentang itu? Bagaimana bidang contoh ditangani, saya belum memeriksanya. Tentu saja saya dapat mencoba semuanya sendiri, tetapi saya mencari pernyataan yang lebih formal dan selalu lebih suka menghindari mengandalkan detail implementasi yang dikerjakan dengan trial and error.

Perbarui 1

Untuk menjawab pertanyaan Remy, penting bagi saya ketika saya perlu menyelesaikan objek di belakang antarmuka sebelum melakukan penyelesaian lainnya.

begin
  AcquirePythonGIL;
  try
    PyObject := CreatePythonObject;
    try
      //do stuff with PyObject
    finally
      Finalize(PyObject);
    end;
  finally
    ReleasePythonGIL;
  end;
end;

Seperti yang tertulis seperti ini, tidak apa-apa. Tetapi dalam kode sebenarnya saya memiliki lokal implisit kedua yang diselesaikan setelah GIL dilepaskan dan dibom. Saya memecahkan masalah dengan mengekstraksi kode di dalam Acquire / Release GIL menjadi metode terpisah dan dengan demikian mempersempit ruang lingkup variabel antarmuka.

David Heffernan
sumber
8
Entah kenapa ini di-downvote, selain itu pertanyaannya sangat kompleks. Dipilih karena telah melampaui kepalaku. Saya tahu persis bahwa sedikit arcanum ini menghasilkan beberapa bug penghitungan referensi halus dalam aplikasi yang saya kerjakan setahun yang lalu. Salah satu geek terbaik kami menghabiskan waktu berjam-jam untuk mencari tahu. Pada akhirnya kami mengatasinya tetapi tidak pernah mengerti bagaimana kompilator dimaksudkan untuk bekerja.
Warren P
3
@Serg Kompilator melakukan penghitungan referensinya dengan sempurna. Masalahnya adalah bahwa ada variabel ekstra yang memiliki referensi yang tidak dapat saya lihat. Yang ingin saya ketahui adalah apa yang memprovokasi kompiler untuk mengambil referensi tambahan yang tersembunyi.
David Heffernan
3
Saya memahami Anda, tetapi praktik yang baik adalah menulis kode yang tidak bergantung pada variabel tambahan tersebut. Biarkan kompiler membuat variabel-variabel ini sesuka hatinya, kode yang solid seharusnya tidak bergantung padanya.
kludg
2
Contoh lain saat ini terjadi:procedure StoreViaAbsoluteToLocal; var I: IInterface; I2: IInterface absolute I; begin I2 := Create; end;
Ondrej Kelle
2
Saya tergoda untuk menyebutnya bug kompilator ... temporaries harus dibersihkan setelah keluar dari ruang lingkup, yang seharusnya menjadi akhir dari tugas (dan bukan akhir dari fungsi). Tidak melakukannya menghasilkan kesalahan halus seperti yang Anda temukan.
nneonneo

Jawaban:

15

Jika ada dokumentasi tentang perilaku ini, mungkin akan berada di area produksi compiler variabel sementara untuk menyimpan hasil antara saat meneruskan hasil fungsi sebagai parameter. Pertimbangkan kode ini:

procedure UseInterface(foo: IInterface);
begin
end;

procedure Test()
begin
    UseInterface(Create());
end;

Compiler harus membuat variabel temp implisit untuk menampung hasil Create saat diteruskan ke UseInterface, untuk memastikan bahwa antarmuka memiliki masa hidup> = masa pakai panggilan UseInterface. Variabel temp implisit itu akan dibuang di akhir prosedur yang memilikinya, dalam hal ini di akhir prosedur Test ().

Ada kemungkinan kasus penugasan pointer Anda jatuh ke dalam keranjang yang sama saat meneruskan nilai antarmuka perantara sebagai parameter fungsi, karena compiler tidak dapat "melihat" ke mana arah nilai tersebut.

Saya ingat ada beberapa bug di area ini selama bertahun-tahun. Dahulu kala (D3? D4?), Compiler tidak menghitung nilai antara sama sekali. Ini berhasil sebagian besar waktu, tetapi mendapat masalah dalam situasi alias parameter. Setelah itu diatasi, ada tindak lanjut terkait const params, saya yakin. Selalu ada keinginan untuk memindahkan pembuangan antarmuka nilai menengah secepat mungkin setelah pernyataan yang diperlukan, tetapi saya rasa hal itu tidak pernah diterapkan di pengoptimal Win32 karena kompilernya tidak disetel up untuk menangani pembuangan pada pernyataan atau perincian blok.

dthorpe.dll
sumber
0

Anda tidak dapat menjamin bahwa compiler tidak akan memutuskan untuk membuat variabel temporal yang tidak terlihat.

Dan bahkan jika Anda melakukannya, pengoptimalan yang dimatikan (atau bahkan tumpukan bingkai?) Dapat mengacaukan kode Anda yang telah diperiksa dengan sempurna.

Dan bahkan jika Anda berhasil meninjau kode Anda di bawah semua kemungkinan kombinasi opsi proyek - mengkompilasi kode Anda di bawah sesuatu seperti Lazarus atau bahkan versi Delphi baru akan membawa neraka kembali.

Taruhan terbaik adalah menggunakan aturan "variabel internal tidak bisa hidup lebih lama dari rutinitas". Kita biasanya tidak tahu, apakah compiler akan membuat beberapa variabel internal atau tidak, tapi kita tahu, bahwa variabel seperti itu (jika dibuat) akan diselesaikan ketika rutinitas ada.

Oleh karena itu, jika Anda memiliki kode seperti ini:

// 1. Some code which may (or may not) create invisible variables
// 2. Some code which requires release of reference-counted data

Misalnya:

Lib := LoadLibrary(Lib, 'xyz');
try
  // Create interface
  P := GetProcAddress(Lib, 'xyz');
  I := P;
  // Work with interface
finally
  // Something that requires all interfaces to be released
  FreeLibrary(Lib); // <- May be not OK
end;

Maka Anda harus menggabungkan blok "Bekerja dengan antarmuka" ke dalam subrutin:

procedure Work(const Lib: HModule);
begin
  // Create interface
  P := GetProcAddress(Lib, 'xyz');
  I := P;
  // Work with interface
end; // <- Releases hidden variables (if any exist)

Lib := LoadLibrary(Lib, 'xyz');
try
  Work(Lib);
finally
  // Something that requires all interfaces to be released
  FreeLibrary(Lib); // <- OK!
end;

Ini adalah aturan yang sederhana, tetapi efektif.

Alex
sumber
Dalam skenario saya, saya: = CreateInterfaceFromLib (...) menghasilkan lokal implisit. Jadi, saran Anda tidak akan membantu. Bagaimanapun, saya sudah menunjukkan dengan jelas solusi untuk pertanyaan itu. Satu berdasarkan seumur hidup penduduk lokal implisit dikendalikan oleh ruang lingkup fungsi. Pertanyaan saya berkaitan dengan skenario yang akan mengarah pada penduduk setempat yang tersirat.
David Heffernan
Maksud saya adalah bahwa ini adalah pertanyaan yang salah untuk ditanyakan sejak awal.
Alex
1
Anda dipersilakan untuk melihat sudut pandang itu tetapi Anda harus mengungkapkannya sebagai komentar. Menambahkan kode yang mencoba (tidak berhasil) untuk mereproduksi solusi dari pertanyaan, tampak aneh bagi saya.
David Heffernan