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 谁持有
─────────────────────────────────────────────────────────────────
- NewInstance 创建闭包 1 隐藏临时变量 temp
- 传给 Send(const ACallback) 1 temp (const 参数不 AddRef)
- PerIoData.Callback := ACallback 2 temp + PerIoData.Callback
- Send 返回 2 temp + PerIoData.Callback
- DirectSend 语句结束,temp Release 1 PerIoData.Callback ← 没归零!
- ...WSASend 在内核排队,线程去干别的...
- _HandleWrite 触发,执行 Callback.Invoke 1 PerIoData.Callback
- _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的高并发性能
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 谁持有
─────────────────────────────────────────────────────────────────
第 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的高并发性能