SilverFruity / OCRunner

Execute Objective-C code as script. AST Interpreter. iOS hotfix SDK.
MIT License
660 stars 149 forks source link

性能比jspatch差5倍的瓶颈在哪里呢? #8

Open luoyibu opened 3 years ago

SilverFruity commented 3 years ago

本质区别: OCRunner采用的是解释执行语法树。 JSPatch采用的是JavaScriptCore来实现的,JIT一应俱全,寄存器虚拟机。 随着同一函数或者方法调用次数的增加,性能差距可能会越来越大。 相关资料: 巨佬资料1 大佬资料2

想了很久,却只能这么说。以后了解得更多了,再来添砖加瓦。😂

luoyibu commented 3 years ago

强 编译原理这块还不大熟,我研究下这两个资料

zyfu0000 commented 3 years ago

本质区别: OCRunner采用的是解释执行语法树。 JSPatch采用的是JavaScriptCore来实现的,JIT一应俱全,寄存器虚拟机。 随着同一函数或者方法调用次数的增加,性能差距可能会越来越大。 相关资料: 巨佬资料1 大佬资料2

想了很久,却只能这么说。以后了解得更多了,再来添砖加瓦。😂

宿主 app 跑 JavaScriptCore 也是没有 JIT 权限的,除非是运行在 wkwebview 里

SilverFruity commented 1 year ago

时隔了两年,今天刚好翻到了这个 issue 这里的 demo 就以下列代码为例

long add(long a, long b) {
  return a+b;
}
  1. 解释执行语法树 以上代码,在 OCRunner 中的执行的伪代码如下:

    NSDictionary *scope;
    MFValue *a = scope[@“a”];
    MFValue *b = scope[@“b”];
    MFValue *result = [MFValue longValue];
    ORBoxValue value;
    switch(a.type) {
    case 'long':
     value.longValue = a.longValue + b.longValue;
     break;
    case ......
    default:
     break;
    }
    result.boxValue = value;
    return result;

    以上均是简化后的 OC 代码,其中还有很多的判断逻辑以及方法调用逻辑(FunctionNode -> BlockNode -> BinaryNode) 以及类型判断等等。 上述代码整个调用链路上使用的汇编代码可谓超级庞大,首当其中的则是在 NSDictionary 中使用 key 获取 objecet. 如果我们将一个 arm 指令定义为一个操作数(耗时),那这里将会存在很多的操作数。

  2. 栈虚拟机 比如 'push args 0' 就是将第一个参数入栈,args 代表的则是一块专属于函数参数的内存区域,0 代表第一个参数 那这里的栈机指令为

    push args 0 // 将 a 压入栈顶,直接通过内存偏移取值
    push args 1 // 将 b 压入栈顶
    addLong // pop a、b,并将 a + b 的值压入栈顶
    ret

    相对于语法树的解释执行,这里已经简化了许多

  3. hash_map 取值切换为了 index 取值,这里的效率提升很明显

  4. 在编译时期,确定了 a + b 的类型,以及最终的值类型,直接使用了特化指令 addLong

或许这么说没有太大的体感。 那么可以看看最终还未优化的 arm64 汇编:

mov args x0

load x1, [x0, #0x0]
store x1, [stack_top]
add stack_stop, stack_top, #0x8 // push a

load x2, [x0, #0x8]
store x1, [stack_top]
add stack_stop, stack_top, #0x8 // push b

load x3, [stack_top]
sub stack_stop, stack_top, #0x8 // pop a

load x4, [stack_top]
sub stack_stop, stack_top, #0x8 // pop b

add x5, x3, x4  //  a +b
store x5, [stack_top]
add stack_stop, stack_top, #0x8 // push result
// 栈帧回溯操作 2-3 个指令
...

至此,我认为解释执行计算密集型的任务性能会有 10 倍差距也属于正常