Skip to content

DelphiCrossSocket大量匿名方法影响性能 #190

Description

@hezihang12

DelphiCrossSocket中存在大量匿名函数调用。匿名函数调用的成本较大,在大并发情况下,匿名函数调用是DelphiCrossSocket的性能瓶颈之一!匿名函数的因为原子操作和堆内存分配释放需要较高成本。

一、reference to procedure 的本质:堆上的引用计数闭包对象
reference to procedure(...) 在编译器内部就是一个接口引用。当你写一个内联的 procedure(...) 且捕获了局部变量时,编译器会隐式生成一个类,大致等价于:

pascal
// 编译器为 DirectSend 里的内联匿名方法自动生成的类(你看不到)
$DirectSendClosure_1234 = class(TInterfacedObject, IInterface)
FSelf: TCrossConnectionBase; // 捕获 Self (为了访问 FOwner)
FLBuffer: Pointer; // 捕获 LBuffer
FACount: Integer; // 捕获 ACount
FACallback: TCrossConnectionCallback;// 捕获 ACallback
procedure Invoke(const AConnection: ICrossConnection; const ASuccess: Boolean);
end;
关键点:

每次执行到这个内联匿名方法,就在堆上 NewInstance 一个对象(GetMem + 初始化)。捕获了哪怕一个变量,就必须每次新建——没法复用单例。
这个对象是 TInterfacedObject 风格,靠引用计数管理生命周期(AddRef/Release,FPC 下编译器自动插桩)。
reference to procedure 变量/参数就是指向这个对象的接口指针。
生成了临时接口对象。

二、异步 IO 完成后释放
关键在于 Send 不是同步的。看 DirectSend 这一行:

pascal
FOwner.Send(Self, LBuffer, ACount, procedure(...) begin ... end);
FOwner.Send 是虚方法,分派到 TSrIocpCrossSocket.Send(或厂商的 TIocpCrossSocket.Send),它的实现是:

pascal
// sr.io.zerocopy.pas:251-257
LPerIoData := _NewIoData;
...
LPerIoData.Callback := ACallback; // ← 把闭包对象的引用存进了 PerIoData!
LPerIoData.Callback := ACallback 这一步,编译器会插入 _AddRef。引用计数的变化轨迹:

text
时刻 闭包对象 refcount 谁持有
─────────────────────────────────────────────────────────────────

  1. NewInstance 创建闭包 1 隐藏临时变量 temp
  2. 传给 Send(const ACallback) 1 temp (const 参数不 AddRef)
  3. PerIoData.Callback := ACallback 2 temp + PerIoData.Callback
  4. Send 返回 2 temp + PerIoData.Callback
  5. DirectSend 语句结束,temp Release 1 PerIoData.Callback ← 没归零!
  6. ...WSASend 在内核排队,线程去干别的...
  7. _HandleWrite 触发,执行 Callback.Invoke 1 PerIoData.Callback
  8. _FreeIoData → PerIoData.Callback := nil 0 无人持有 → Free! ← 这里才释放
    第 5 步 Send 已经返回了,但闭包对象还活着——因为 PerIoData 还拿着它。它要一直活到 WSASend 完成 → _HandleWrite 调用 Callback.Invoke → _FreeIoData 把 Callback 置 nil,才真正释放。

这是异步回调模型决定的:闭包对象的生命周期 = 异步 I/O 的生命周期,不是调用方的生命周期。

三、每次调用的真实开销
所以 DirectSend 这一次调用的隐形成本是:

堆分配(闭包对象 NewInstance) :1 次( 创建内联匿名方法时)
_AddRef :1次(|存进 PerIoData.Callback 时)
_Release :1 次( temp 语句结束)
_Release : 1 次( PerIoData 释放时)
堆释放(FreeInstance) :1 次( 引用计数归零时)
= 每次匿名调用 1 次堆 alloc + 1 次堆 free + 2 次原子 AddRef/Release。

而且这个闭包对象还会跨线程——在 IOCP worker 线程上 _HandleWrite 里被调用、被 Release。接口引用计数用的是 InterLockedIncrement/Decrement(原子操作),两次 AddRef/Release 原子操作。堆分配和释放,内部也存在原子操作和锁的操作。
综上,大量使用匿名函数,严重影响DelphiCrossSocket的高并发性能

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions