Summary or problem description
Our NGD protocol team is working on tps improvement.
In this process,we find that we use function pointer to invoke a interop service.This will generate a lot of extra cost.We have do a test:
```c#
class Program
{
private static readonly Dictionary
static void Main(string[] args)
{
Neo.SmartContract.InteropDescriptor1 descriptor = new InteropDescriptor1("doOnceWork", doOnceWork);
methods.Add("doOnceWork", descriptor);
if (!methods.TryGetValue("doOnceWork", out Neo.SmartContract.InteropDescriptor1 descriptor1))
{
return;
}
System.Diagnostics.Stopwatch stopwatchFunc = new System.Diagnostics.Stopwatch();
stopwatchFunc.Start();
for (int i = 0; i < 10000; i++)
{
descriptor1.Handler(null);
}
stopwatchFunc.Stop();
Console.WriteLine("FunInv timeSpan:" + stopwatchFunc.Elapsed.TotalSeconds);
stopwatchFunc.Reset();
stopwatchFunc.Start();
for (int i = 0; i < 10000; i++)
{
if ("doOnceWork".Equals("doOnceWork"))
{
doOnceWork(null);
}
}
stopwatchFunc.Stop();
Console.WriteLine("Normal timeSpan" + stopwatchFunc.Elapsed.TotalSeconds);
stopwatchFunc.Reset();
}
public static bool doOnceWork(string engine)
{
return true;
}
}
```c#
internal class InteropDescriptor1
{
public string Method { get; }
public Func<String, bool> Handler { get; }
public InteropDescriptor1(string method, Func<String, bool> handler)
{
this.Method = method;
this.Handler = handler;
}
}
In our test,we find 10000 times invoke by function pointer way will cost 0.000687 s.And if we use a normal way,it will cost 0.000057799 s. Almost 10 times the gap is .

And we have analyzed which process cost the most time and lower the tps. We find that it is descriptor.Handler(engine).
https://github.com/neo-project/neo/blob/72be7f6a26f3ed222f1ad5dc1b4391c62fa4f67d/neo/SmartContract/InteropService.cs#L131-L138
So we think if we change invoking InteropService from the function pointer way to a normal way, Tps will have a great improvement.
Do you have any solution you want to propose?
Change invoking InteropService from the function pointer way to a normal way
Where in the software does this update applies to?
Did you try with a delegate?
You are testing with one entry, to be sure you should put more entries in both tests
@shargon
After a hardcore test,it is faster in normal way.hahaha
```c#
using Neo.SmartContract;
using System;
using System.Collections.Generic;
namespace FuncTest
{
class Program
{
private static readonly Dictionary
static void Main(string[] args)
{
Neo.SmartContract.InteropDescriptor1 descriptor = new InteropDescriptor1(0, doOnceWork);
for (int i = 0; i < 50; i++) {
methods.Add(i, descriptor);
}
System.Diagnostics.Stopwatch stopwatchFunc = new System.Diagnostics.Stopwatch();
stopwatchFunc.Start();
if (!methods.TryGetValue(0, out Neo.SmartContract.InteropDescriptor1 descriptor1))
{
return;
}
for (int i = 0; i < 10000; i++)
{
descriptor1.Handler(null);
}
stopwatchFunc.Stop();
Console.WriteLine("FunInv timeSpan:" + stopwatchFunc.Elapsed.TotalSeconds);
stopwatchFunc.Reset();
stopwatchFunc.Start();
int a = 50;
for (int i = 0; i < 10000; i++)
{
switch (a) {
case 0:
doOnceWork(null);
continue;
case 1:
doOnceWork(null);
continue;
case 2:
doOnceWork(null);
continue;
case 3:
doOnceWork(null);
continue;
case 4:
doOnceWork(null);
continue;
case 5:
doOnceWork(null);
continue;
case 6:
doOnceWork(null);
continue;
case 7:
doOnceWork(null);
continue;
case 8:
doOnceWork(null);
continue;
case 9:
doOnceWork(null);
continue;
case 10:
doOnceWork(null);
continue;
case 11:
doOnceWork(null);
continue;
case 12:
doOnceWork(null);
continue;
case 13:
doOnceWork(null);
continue;
case 14:
doOnceWork(null);
continue;
case 15:
doOnceWork(null);
continue;
case 16:
doOnceWork(null);
continue;
case 17:
doOnceWork(null);
continue;
case 18:
doOnceWork(null);
continue;
case 19:
doOnceWork(null);
continue;
case 20:
doOnceWork(null);
continue;
case 21:
doOnceWork(null);
continue;
case 22:
doOnceWork(null);
continue;
case 23:
doOnceWork(null);
continue;
case 24:
doOnceWork(null);
continue;
case 25:
doOnceWork(null);
continue;
case 26:
doOnceWork(null);
continue;
case 27:
doOnceWork(null);
continue;
case 28:
doOnceWork(null);
continue;
case 29:
doOnceWork(null);
continue;
case 30:
doOnceWork(null);
continue;
case 31:
doOnceWork(null);
continue;
case 32:
doOnceWork(null);
continue;
case 33:
doOnceWork(null);
continue;
case 34:
doOnceWork(null);
continue;
case 35:
doOnceWork(null);
continue;
case 36:
doOnceWork(null);
continue;
case 37:
doOnceWork(null);
continue;
case 38:
doOnceWork(null);
continue;
case 39:
doOnceWork(null);
continue;
case 40:
doOnceWork(null);
continue;
case 41:
doOnceWork(null);
continue;
case 42:
doOnceWork(null);
continue;
case 43:
doOnceWork(null);
continue;
case 44:
doOnceWork(null);
continue;
case 45:
doOnceWork(null);
continue;
case 46:
doOnceWork(null);
continue;
case 47:
doOnceWork(null);
continue;
case 48:
doOnceWork(null);
continue;
case 49:
doOnceWork(null);
continue;
case 50:
doOnceWork(null);
continue;
default:
continue;
}
}
stopwatchFunc.Stop();
Console.WriteLine("Normal timeSpan" + stopwatchFunc.Elapsed.TotalSeconds);
stopwatchFunc.Reset();
}
public static bool doOnceWork(string engine)
{
return true;
}
}
}
```

the time is wasted in the TryGet or in the invoke?
In invoke.
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Neo.SmartContract;
using System;
using System.Diagnostics;
namespace Neo.UnitTests
{
[TestClass]
public class Test
{
[TestMethod]
public void TestBench()
{
var descriptor = new InteropDescriptor("Test", doOnceWork, 0, TriggerType.Application);
var stopwatchFunc = Stopwatch.StartNew();
for (var i = 0; i < 10_000; i++)
{
descriptor.Handler.Invoke(null);
}
stopwatchFunc.Stop();
Console.WriteLine("FunInv timeSpan:" + stopwatchFunc.Elapsed.TotalMilliseconds);
stopwatchFunc = Stopwatch.StartNew();
for (var i = 0; i < 10_000; i++)
{
doOnceWork(null);
}
stopwatchFunc.Stop();
Console.WriteLine("Normal timeSpan :" + stopwatchFunc.Elapsed.TotalMilliseconds);
stopwatchFunc.Reset();
}
public static bool doOnceWork(ApplicationEngine engine) => true;
}
}
Yes, dynamic calll is slower
Release results:
FunInv timeSpan:0.0631
Normal timeSpan: 0.0031
So we want to change to a normal way.We will make a pr soon.
Current,one VM execution will cost 0.3ms. And more than 60% time is spent on dynamic calll.If we change this,tps will have a great improvement.
Before change it, check if there are faster dynamic call alternatives (I think that is not)
OK
We should avoid the initial cost during the benchmarks
delegate bool delegateCall(ApplicationEngine engine);
[TestMethod]
public void TestBench()
{
// init to avoid initialization cost
doOnceWork(null);
var descriptor = (Func<ApplicationEngine, bool>)doOnceWork;
var del = new delegateCall(doOnceWork);
descriptor.Invoke(null);
del.Invoke(null);
del(null);
// Benchmarks
var stopwatchFunc = Stopwatch.StartNew();
for (var i = 0; i < 10_000; i++)
{
descriptor.Invoke(null);
}
stopwatchFunc.Stop();
Console.WriteLine("Invoke:" + stopwatchFunc.Elapsed.TotalMilliseconds);
stopwatchFunc = Stopwatch.StartNew();
for (var i = 0; i < 10_000; i++)
{
del.Invoke(null);
}
stopwatchFunc.Stop();
Console.WriteLine("Delegate-Invoke:" + stopwatchFunc.Elapsed.TotalMilliseconds);
stopwatchFunc = Stopwatch.StartNew();
for (var i = 0; i < 10_000; i++)
{
del(null);
}
stopwatchFunc.Stop();
Console.WriteLine("Delegate-Call:" + stopwatchFunc.Elapsed.TotalMilliseconds);
stopwatchFunc = Stopwatch.StartNew();
for (var i = 0; i < 10_000; i++)
{
doOnceWork(null);
}
stopwatchFunc.Stop();
Console.WriteLine("Normal:" + stopwatchFunc.Elapsed.TotalMilliseconds);
stopwatchFunc.Reset();
}
public static bool doOnceWork(ApplicationEngine engine) => true;
Also is faster the direct calls, but we should decide if it worth the loose of the readability
Invoke: 0.0281
Delegate-Invoke: 0.0262
Delegate-Call: 0.0284
Normal: 0.0032
Got it.I think we should use some design pattern here.We will try to design it.
@shargon
We have design two implement: 1.use switch-case way 2.use interface
These two way cost almost same time. Which implement do you like?
Way 1:This way use switch-case.It will have many hardcode
```c#
using System;
using System.Collections.Generic;
namespace FuncTest.way1
{
public class InteropService
{
Contract1 conttract1 = Contract1.getInstance();
public bool Func1(string parameter)
{
Console.WriteLine("invoke InteropService Fun1");
return true;
}
public bool Func2(string parameter)
{
Console.WriteLine("invoke InteropService Fun2");
return true;
}
public bool Func3(string parameter) {
Console.WriteLine("invoke InteropService Fun3");
return true;
}
public bool Invoke(string parameter, int method)
{
var ret = false;
switch (method) {
case 1:
ret=Func1(parameter);
break;
case 2:
ret = Func2(parameter);
break;
case 3:
ret = Func3(parameter);
break;
case 11:
ret = conttract1.Invoke(parameter,method);
break;
case 12:
ret = conttract1.Invoke(parameter, method);
break;
case 13:
ret = conttract1.Invoke(parameter, method);
break;
default:
break;
}
return ret;
}
}
}
```c#
using System;
using System.Collections.Generic;
using System.Text;
namespace FuncTest.way1
{
public interface IFuncInvoke
{
bool Invoke(string parameter,int method);
}
}
```c#
using FuncTest.way1;
using System;
using System.Collections.Generic;
using System.Text;
namespace FuncTest.way1
{
public abstract class NativeContract : IFuncInvoke
{
public virtual bool Invoke(string parameter, int method)
{
throw new NotImplementedException();
}
}
}
```c#
using System;
using System.Collections.Generic;
using System.Text;
namespace FuncTest.way1
{
public class Contract1:NativeContract
{
private static Contract1 singleInstance=new Contract1();
private Contract1() { }
public static Contract1 getInstance()
{
return singleInstance;
}
public override bool Invoke(string parameter, int method)
{
var ret = false;
switch (method)
{
case 11:
ret = Func1(parameter);
break;
case 12:
ret = Func2(parameter);
break;
case 13:
ret = Func3(parameter);
break;
default:
break;
}
return ret;
}
public bool Func1(string parameter)
{
return true;
}
public bool Func2(string parameter)
{
return true;
}
public bool Func3(string parameter)
{
return true;
}
}
}
Way 2:This way use interface.It is more elegant。
```c#
using FuncTest.way2;
using System;
using System.Collections.Generic;
namespace Neo.SmartContract
{
public class InteropService
{
internal class Func1 : IFuncInvoke
{
public bool Invoke(string parameter)
{
return true;
}
}
internal class Func2 : IFuncInvoke
{
public bool Invoke(string parameter)
{
return true;
}
}
internal class Func3 : IFuncInvoke
{
public bool Invoke(string parameter)
{
return true;
}
}
public bool Invoke(string parameter, int method)
{
if (!methods.TryGetValue(method, out IFuncInvoke func))
{
return false;
}
var ret=func.Invoke(parameter);
return ret;
}
public InteropService() {
Register(1, new Func1());
Register(2, new Func2());
Register(3, new Func3());
foreach (KeyValuePair<int, IFuncInvoke> item in NativeContract.methods) {
Register(item.Key, item.Value);
}
}
private static Dictionary<int, IFuncInvoke> methods = new Dictionary<int, IFuncInvoke>();
private static void Register(int name, IFuncInvoke func)
{
methods.Add(name, func);
}
}
}
```c#
using System;
using System.Collections.Generic;
using System.Text;
namespace FuncTest.way2
{
public interface IFuncInvoke
{
bool Invoke(string parameter);
}
}
```c#
using System;
using System.Collections.Generic;
using System.Text;
namespace FuncTest.way2
{
public class NativeContract
{
public static Dictionary
public static Contract1 contract1=new Contract1();
public void Register(int name,IFuncInvoke func) {
methods.Add(name, func);
}
}
}
```c#
using System;
using System.Collections.Generic;
using System.Text;
namespace FuncTest.way2
{
public class Contract1:NativeContract
{
public Contract1()
{
Register(11, new NativeFunc1());
Register(12, new NativeFunc2());
Register(13, new NativeFunc3());
}
internal class NativeFunc1 : IFuncInvoke
{
public bool Invoke(string parameter)
{
return true;
}
}
internal class NativeFunc2 : IFuncInvoke
{
public bool Invoke(string parameter)
{
return true;
}
}
internal class NativeFunc3 : IFuncInvoke
{
public bool Invoke(string parameter)
{
return true;
}
}
}
}

I prefer interfaces, but i think that we should vote. I saw complex methods using IL code hahaha but I think that it's too complex
I vote for way 2, which is more quick. 😆
@erikzhang
Hi,da lao. Do you have some opinion about this?
option 2
Delegate call is slower only when the first time you use it. If you increase the iteration count you will get a very similar performance result.
Here are my test result:
FunInv timeSpan:0.0001557
Normal timeSpan4.18E-05
FunInv timeSpan:0.0005847
Normal timeSpan0.0003595
FunInv timeSpan:0.0049434
Normal timeSpan0.0041861
In conclusion, maybe we need some way to "warm-up" all the interposervice methods?
I prefer the original way. It is very readable and the performance loss is in an acceptable range.
Maybe changing the dynamic call to a delegate it's enough.
There is no dynamic call. It's delegate:
But this is fast as a real delegate?
It is a real delegate. 🤣
Waiting for the benchmark, if this method can increase more than 15%+ performance, we may consider it.
After testing, we found that there is not much difference in time consumption between the ordinary call method and the delegate call method. The delegate call only takes more time when it is first called, but the time consumption is not large after multiple calls.
We have also rewritten the interopService part in way of normal call and found that the test results after rewriting are not much different from the previous ones, so we consider closing this issue.
Most helpful comment
I vote for way 2, which is more quick. 😆