问题 有没有办法在编译时声明一个方法的const引用?


我正在编写一个脚本解释器,我已经设法进入工作状态。它有一个解析脚本并生成字节码的编译器,以及一个执行字节码的VM。

翻译的核心是一个巨大的循环 case 看起来像这样的语句:

case CurrentOpcode.Operation of
  OP_1: DoOp1(CurrentOpcode);
  OP_2: DoOp2(CurrentOpcode);
  ...
  OP_N: DoOpN(CurrentOpcode);
end;

分析告诉我,无论出于何种原因,我的脚本执行都花费了大量时间 case 声明,这对我来说很奇怪,所以我正在寻找一种优化它的方法。显而易见的解决方案,因为所有操作函数基本上具有相同的签名,就是创建一个由操作码索引的方法指针数组。 Operation 值。但 Operation 被声明为枚举,并且能够将其声明为const数组会很好,这样如果我将来添加更多的操作码,编译器可以提醒我更新数组。

由于方法指针存储运行时状态,( Self 引用它运行的对象,)我无法创建方法指针的const数组。 (无论如何这也不是一个好主意,因为很可能我最终会同时运行多个脚本。)但方法只是语法糖。就像是:

procedure TMyObject.DoSomething(x, y: integer);

真正意思:

procedure TMyObject_DoSomething(Self: TMyObject; x, y: integer);

所以我应该能够以后一种形式声明一个函数指针类型并以那种方式分配它,然后我只需要显式传递 Self 作为我调用它时的第一个参数。但编译器并不喜欢这样。

type TOpcodeProc = procedure (Self: TScriptVM; Opcode: TOpcode);
const OPCODE: TOpcodeProc = TScriptVM.DoOp1;

[DCC Error]: E2009 Incompatible types: 'regular procedure and method pointer'

我已经尝试了不同的变体来尝试让它编译,但它们都会给出错误。有没有办法让这个编译?


4810
2018-02-05 23:07


起源

处理这个问题的另一种方法是用一个CurrentOperation类的后代列表替换枚举。而不是设置Operation,创建操作码特定后代类的实例。使 DoOp 类的虚拟(抽象)方法,然后用单个语句替换整个case语句: CurrentOperation.DoOp。您可以使用操作码枚举索引的类引用数组来决定实例化哪个类。 - Rob Kennedy
@Rob:是的,这可能会有效,但考虑到这里所述的目标是提高性能,将当前情况转换为类似的东西,这会增加对象创建和虚拟调度的显着开销,实际上听起来不像一个好主意... - Mason Wheeler
真的会有多少开销?您将使用数组查找类引用。你要创建一个对象,无论如何你已经在做了。虚拟调度只是一个指针查找,无论如何你都可以从你提出的将部分方法指针存储在数组中的想法中获得。 - Rob Kennedy
@Rob:当前系统中没有对象创建。操作码是非常简单的记录,使序列化更容易。 (脚本“program”是一个操作码记录数组,所以整个事物可以被块写入流并以相同的方式回读。) - Mason Wheeler
@Mason:你看过它是如何编译的吗?恕我直言的典型汇编代码(和Delphi生成一个)这样的case语句是跳转表和'indexed'跳转如下:cmp cmp edx,lowerbound, jnbe exit, jmp dword ptr [edx*4 + tableoffset] 。恕我直言,如果这个陈述浪费时间,它应该通过CurrentOpCode(它的类型是什么 - 记录?如果是,它按值传递并每次复制)。 - pf1957


答案:


宣言:

const
  OPCODE: array[TOperation] of Pointer = (
    @TScriptVM.DoOp1, 
    @TScriptVM.DoOp2, 
    ... 
    @TScriptVM.DoOpN
  );

呼叫:

TOpcodeProc(OPCODE[CurrentOpcode.Operation])(Self, CurrentOpcode);

更酷的东西:

var
  OpCodeProcs: array[TOperation] of TOpCodeProc absolute OPCODE;

调用该方法的语法更好:

OpCodeProcs[CurrentOpcode.Operation](Self, CurrentOpcode);

好的是因为编译器阻止你为OpCodeProcs变量赋值而绝对不变!


9
2018-02-06 08:26



您可以在方法中隐藏调用并内联方法。喜欢 : procedure TScriptVM.DoOp( CurrentOpCode : TCurrentOperation); inline; 和实施: procedure TScriptVM.DoOp( CurrentOpCode : TCurrentOperation); begin OpCodeProcs[CurrentOpCode.Operation](Self, CurrentOpcode); end;。看起来好一点,几乎同样快。 - LU RD


对于问题的不变部分没有解决方案,但是这是你可以消除的问题 case

type
  TTestMethod = procedure(Instance: TObject; Param1, Param2: Integer);

  TTest = class(TObject)
  private
    FMethods: array[0..1] of TTestMethod;
    procedure InitMethods;
    procedure CallMethod(ID: Integer; Param1, Param2: Integer);
  protected
    procedure TestMethod0(Param1, Param2: Integer);
    procedure TestMethod1(Param1, Param2: Integer);
  end;

procedure TTest.InitMethods;
begin
  FMethods[0] := TTestMethod(@TTest.TestMethod0);
  FMethods[1] := TTestMethod(@TTest.TestMethod1);
end;

procedure TTest.CallMethod(ID: Integer; Param1, Param2: Integer);
begin
  FMethods[ID](Self, Param1, Param2);
end;

5
2018-02-05 23:27



Mason非常清楚他已经知道如何在运行时填充数组。 - David Heffernan