การจัดการคอมไพเลอร์ของตัวแปรอินเทอร์เฟซโดยนัยมีการบันทึกไว้หรือไม่?


86

ฉันถามคำถามที่คล้ายกันเกี่ยวกับตัวแปรอินเทอร์เฟซโดยนัยเมื่อไม่นานมานี้

ที่มาของคำถามนี้คือข้อบกพร่องในโค้ดของฉันเนื่องจากฉันไม่ทราบถึงการมีอยู่ของตัวแปรอินเทอร์เฟซโดยนัยที่สร้างโดยคอมไพเลอร์ ตัวแปรนี้ได้รับการสรุปเมื่อขั้นตอนที่เป็นเจ้าของเสร็จสิ้น สิ่งนี้ทำให้เกิดข้อผิดพลาดเนื่องจากอายุการใช้งานของตัวแปรนานกว่าที่ฉันคาดไว้

ตอนนี้ฉันมีโครงการง่ายๆที่จะแสดงพฤติกรรมที่น่าสนใจบางอย่างจากคอมไพเลอร์:

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.

StoreToLocalรวบรวมตามที่คุณคิด ตัวแปรท้องถิ่นIผลฟังก์ชั่นที่จะถูกส่งเป็นนัยพารามิเตอร์var Createขึ้นเป็นระเบียบเรียบร้อยสำหรับผลในสายเดียวที่จะStoreToLocal IntfClearไม่มีความประหลาดใจที่นั่น

อย่างไรก็ตามStoreViaPointerToLocalได้รับการปฏิบัติที่แตกต่างกัน Createคอมไพเลอร์สร้างตัวแปรท้องถิ่นนัยที่มันผ่านไป เมื่อCreateส่งคืนการมอบหมายให้P^จะดำเนินการ ซึ่งจะทำให้รูทีนมีตัวแปรโลคัลสองตัวที่มีการอ้างอิงถึงอินเทอร์เฟซ ความเป็นระเบียบเรียบร้อยสำหรับStoreViaPointerToLocalผลลัพธ์ในการโทรสองIntfClearครั้ง

โค้ดที่คอมไพล์แล้วStoreViaPointerToLocalเป็นดังนี้:

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 

ฉันเดาได้ว่าทำไมคอมไพเลอร์ถึงทำเช่นนี้ เมื่อสามารถพิสูจน์ได้ว่าการกำหนดให้กับตัวแปรผลลัพธ์จะไม่ทำให้เกิดข้อยกเว้น (เช่นถ้าตัวแปรเป็นโลคัล) ก็จะใช้ตัวแปรผลลัพธ์โดยตรง มิฉะนั้นจะใช้อินเทอร์เฟซโดยนัยและคัดลอกอินเทอร์เฟซเมื่อฟังก์ชันกลับมาแล้วดังนั้นจึงมั่นใจได้ว่าเราจะไม่รั่วไหลการอ้างอิงในกรณีที่มีข้อยกเว้น

แต่ฉันไม่พบข้อความใด ๆ ในเอกสารนี้ เป็นเรื่องสำคัญเนื่องจากอายุการใช้งานของอินเทอร์เฟซเป็นสิ่งสำคัญและในฐานะโปรแกรมเมอร์คุณต้องสามารถมีอิทธิพลต่อมันได้ในบางโอกาส

มีใครทราบบ้างว่ามีเอกสารเกี่ยวกับพฤติกรรมนี้หรือไม่? ถ้าไม่มีใครมีความรู้เพิ่มเติมหรือไม่? ช่องอินสแตนซ์ได้รับการจัดการอย่างไรฉันยังไม่ได้ตรวจสอบ แน่นอนว่าฉันสามารถลองใช้ทั้งหมดได้ด้วยตัวเอง แต่ฉันกำลังมองหาคำชี้แจงที่เป็นทางการมากกว่านี้และมักจะชอบที่จะหลีกเลี่ยงการพึ่งพารายละเอียดการนำไปใช้งานโดยการลองผิดลองถูก

อัปเดต 1

ในการตอบคำถามของ Remy สิ่งนี้มีความสำคัญสำหรับฉันเมื่อฉันต้องทำให้วัตถุที่อยู่ด้านหลังอินเทอร์เฟซเสร็จสิ้นก่อนที่จะดำเนินการขั้นสุดท้ายอีกครั้ง

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

พอเขียนแบบนี้ก็สบายดี แต่ในรหัสจริงฉันมีภาษาท้องถิ่นโดยนัยที่สองซึ่งสรุปได้หลังจากที่ GIL ถูกปล่อยออกมาและระเบิดนั้น ฉันแก้ไขปัญหาโดยการแยกโค้ดภายใน Acquire / Release GIL ออกเป็นวิธีการแยกต่างหากและทำให้ขอบเขตของตัวแปรอินเทอร์เฟซแคบลง


8
ไม่รู้ว่าเหตุใดจึงถูกลดคะแนนนอกจากคำถามนั้นซับซ้อนมาก โหวตให้เป็นทางเหนือหัวของฉัน ฉันรู้ว่า arcanum เล็กน้อยนี้ส่งผลให้เกิดข้อบกพร่องในการนับการอ้างอิงที่ละเอียดอ่อนในแอพที่ฉันทำงานเมื่อปีที่แล้ว หนึ่งในผู้เชี่ยวชาญที่ดีที่สุดของเราใช้เวลาหลายชั่วโมงในการคิดออก ในที่สุดเราก็แก้ไขมันได้ แต่ไม่เคยเข้าใจว่าคอมไพเลอร์ตั้งใจทำงานอย่างไร
Warren P

3
@Serg คอมไพเลอร์ทำการอ้างอิงการนับอย่างสมบูรณ์แบบ ปัญหาคือมีตัวแปรพิเศษที่มีการอ้างอิงที่ฉันมองไม่เห็น สิ่งที่ฉันอยากรู้คือสิ่งที่กระตุ้นให้คอมไพเลอร์ใช้การอ้างอิงเพิ่มเติมซ่อนเร้น
David Heffernan

3
ฉันเข้าใจคุณ แต่แนวทางปฏิบัติที่ดีคือการเขียนโค้ดที่ไม่ขึ้นอยู่กับตัวแปรพิเศษดังกล่าว ปล่อยให้คอมไพเลอร์สร้างตัวแปรเหล่านี้มากเท่าที่ต้องการโค้ดที่เป็นของแข็งไม่ควรขึ้นอยู่กับมัน
kludg

2
อีกตัวอย่างหนึ่งเมื่อสิ่งนี้เกิดขึ้น:procedure StoreViaAbsoluteToLocal; var I: IInterface; I2: IInterface absolute I; begin I2 := Create; end;
Ondrej Kelle

2
ฉันอยากจะเรียกสิ่งนี้ว่าบั๊กของคอมไพเลอร์ ... จังหวะควรถูกล้างหลังจากที่มันออกไปนอกขอบเขตซึ่งควรจะเป็นจุดสิ้นสุดของการมอบหมาย (ไม่ใช่จุดสิ้นสุดของฟังก์ชัน) การไม่ทำเช่นนั้นก่อให้เกิดข้อผิดพลาดเล็กน้อยอย่างที่คุณค้นพบ
nneonneo

คำตอบ:


15

หากมีเอกสารเกี่ยวกับลักษณะการทำงานนี้อาจอยู่ในพื้นที่ของการผลิตคอมไพเลอร์ของตัวแปรชั่วคราวเพื่อเก็บผลลัพธ์ระดับกลางเมื่อส่งผลลัพธ์ของฟังก์ชันเป็นพารามิเตอร์ พิจารณารหัสนี้:

procedure UseInterface(foo: IInterface);
begin
end;

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

คอมไพเลอร์ต้องสร้างตัวแปร temp โดยนัยเพื่อเก็บผลลัพธ์ของ Create เมื่อส่งผ่านไปยัง UseInterface เพื่อให้แน่ใจว่าอินเทอร์เฟซมีอายุการใช้งาน> = อายุการใช้งานของการเรียก UseInterface ตัวแปร temp โดยนัยนั้นจะถูกกำจัดเมื่อสิ้นสุดขั้นตอนที่เป็นเจ้าของในกรณีนี้เมื่อสิ้นสุดขั้นตอน Test ()

เป็นไปได้ว่ากรณีการกำหนดพอยน์เตอร์ของคุณอาจตกอยู่ในที่เก็บข้อมูลเดียวกับการส่งผ่านค่าอินเทอร์เฟซกลางเป็นพารามิเตอร์ฟังก์ชันเนื่องจากคอมไพเลอร์ไม่สามารถ "มองเห็น" ว่าค่าจะไปที่ใด

ฉันจำได้ว่ามีข้อบกพร่องบางอย่างในพื้นที่นี้ในช่วงหลายปีที่ผ่านมา นานมาแล้ว (D3? D4?) คอมไพลเลอร์ไม่ได้อ้างอิงถึงการนับค่ากลางเลย ใช้งานได้เกือบตลอดเวลา แต่มีปัญหาในสถานการณ์นามแฝงพารามิเตอร์ เมื่อได้รับการแก้ไขแล้วมีการติดตามผลเกี่ยวกับ const params ฉันเชื่อว่า มีความปรารถนาที่จะย้ายการกำจัดของอินเทอร์เฟซค่ากลางโดยเร็วที่สุดหลังจากคำสั่งที่จำเป็น แต่ฉันไม่คิดว่าจะมีการใช้งานในเครื่องมือเพิ่มประสิทธิภาพ Win32 เนื่องจากคอมไพเลอร์ไม่ได้ตั้งค่าไว้ ขึ้นสำหรับการจัดการการกำจัดตามคำสั่งหรือความละเอียดของบล็อก


0

คุณไม่สามารถรับประกันได้ว่าคอมไพเลอร์จะไม่ตัดสินใจสร้างตัวแปรที่มองไม่เห็นชั่วขณะ

และแม้ว่าคุณจะทำเช่นนั้นการปิดการเพิ่มประสิทธิภาพ (หรือแม้แต่สแต็กเฟรม?) อาจทำให้โค้ดที่ตรวจสอบอย่างสมบูรณ์ของคุณเสียหายได้

และแม้ว่าคุณจะจัดการตรวจสอบโค้ดของคุณภายใต้การรวมกันของตัวเลือกโปรเจ็กต์ที่เป็นไปได้ทั้งหมด - การรวบรวมโค้ดของคุณภายใต้อะไรบางอย่างเช่น Lazarus หรือแม้แต่ Delphi เวอร์ชันใหม่ก็จะนำนรกกลับมา

ทางออกที่ดีที่สุดคือใช้กฎ "ตัวแปรภายในไม่สามารถอยู่ได้นานกว่ารูทีน" โดยปกติเราไม่รู้ว่าคอมไพเลอร์จะสร้างตัวแปรภายในหรือไม่ แต่เรารู้ว่าตัวแปรดังกล่าว (ถ้าสร้าง) จะถูกสรุปเมื่อมีรูทีนอยู่

ดังนั้นหากคุณมีรหัสเช่นนี้:

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

เช่น:

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;

จากนั้นคุณควรรวมบล็อก "Work with interface" ไว้ในรูทีนย่อย:

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;

เป็นกฎง่ายๆ แต่ได้ผล


ในสถานการณ์ของฉัน I: = CreateInterfaceFromLib (... ) ส่งผลให้เกิด local โดยนัย ดังนั้นสิ่งที่คุณแนะนำจะไม่ช่วย ไม่ว่าในกรณีใดฉันได้แสดงให้เห็นอย่างชัดเจนถึงวิธีแก้ปัญหาในคำถามนี้ หนึ่งตามอายุการใช้งานของชาวบ้านโดยนัยที่ถูกควบคุมโดยขอบเขตการทำงาน คำถามของฉันเกี่ยวข้องกับสถานการณ์ที่จะนำไปสู่ชาวบ้านโดยปริยาย
David Heffernan

ประเด็นของฉันคือนี่เป็นคำถามที่ผิดที่จะถามตั้งแต่แรก
Alex

1
คุณสามารถเข้าสู่จุดชมวิวนั้นได้ แต่คุณควรแสดงความคิดเห็น การเพิ่มรหัสที่พยายาม (ไม่สำเร็จ) เพื่อสร้างวิธีแก้ปัญหาของคำถามซ้ำดูเหมือนจะเป็นเรื่องแปลกสำหรับฉัน
David Heffernan
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.