Skip to content

Performance Tips

Kameron Brooks edited this page Nov 29, 2018 · 8 revisions

Speed

The speed of your CCL script depends on a few factors. There are may performance tricks and optimizations in the compiler that make the scripts run as fast as possible. The performance of compiled ccl code is 10x to 100x slower than that of straight c# code. That sounds bad, but it is actually pretty fast compared to straight vanilla c# reflection which can be 100x to 500x slower than straight C#.

With that being said, the CCL engine still runs on Reflection, and even with all the optimizations, reflection will still be the slowest part of your program.

How to avoid Reflection

Heavy use of a variable or method passed in via context will usually be slower than using a compile-time variable. For example, if vec.x is passed in via the context

for(int i=0; i < 100; i += 1) {
   vec.x += 1;
}

Will be slower than

int x = vec.x;
for(int i=0; i < 100; i += 1) {
   x += 1;
}
vec.x = x;

This is not always the case, delegate and method caching can sometimes make function calls and member access faster (I will dive into this more in the next section)

Method Caching

When your code tries to access a member of an object, CCL has to use reflection to find that member on the object. There is some overhead associated with that search. What CCL then does, is cache that method in a table in case it needs to be used again. If the method, property, or field of that object type needs to be accessed again, it will be loaded from the lookup table instead of using Reflection. Once the member is cached, there is a sizable performance boost. This performance increase is especially dramatic when the member is used in a loop.

Delegate Caching

If a method or member access matches a predefined type, then a delegate can be cached and called like a regular function instead of called using invoke.

When using Reflection, the most time consuming part seems to be calling functions with Invoke. Fetching the method is not quite as slow as actually invoking it. There are 2 ways around this but each has a drawback.

  1. You can create an closed instance delegate, cast it to the correct type, and then call it like a regular function. This yields a pretty amazing performance increase. For example, 50ms instead of 500ms

  2. You can create an open delegate, cast it to the the correct type, and pass in the calling object as the first argument and then the normal arguments This yields an incredible performance increase. For example, 10ms instead of 500ms unfortunately open delegates are out of the picture as There are a few major drawbacks to these approaches:

First issue, the process of creating an instance delegate and caching it takes more time than just invoking the method... so if you are only going to call the method less than thrice, it is not worth caching it.

Second, for both approaches, you must know the delegate type ahead of time. The only way you can get away without using invoke is if you can cast the function to the correct type. So the type must be known before hand. This can be achieved by looking up a function type in a dictionary of predefined function types that are likely to be used and calling its respective cast. Signatures for all the normal primitive types are stored in the assembly, like:

Action<>
Action<bool>
Action<int>
...
Func<int>
Func<float>
...
Func<int,int>
etc...

If a function signature is not in the assembly, then it cannot be called using this trick and must be called with Invoke (which is slow)

What that means is that functions that are not primitive types or generic object class, cannot be known ahead of time and thus cannot take advantage of this performance increase. A method of type Func<int,MyClass1> will not be found in the assembly, and must be called with Invoke. The CCL engine does support methods of type Func<object,object>, so if you make the method signature match that and then cast the objects to the appropriate type from within the function, you can take advantage of this performance increase.

For example,

class DemoContext {
   // This will be optimized because the signature is in the assembly 
   int Add(int x, int y) {
      return x + y;
   }
   // This will not be optimized because the signature is not in the assembly 
   // This will be slower because it will have to be called with Invoke()
   int Add(int x, MyClass1 y) {
      return x + y.member1;
   }

   // This will be optimized because the signature is in the assembly 
   // Though there are certain costs to casting and boxing/unboxing
   int Add(object x, object y) {
      return (int)x + ((MyClass1)y).member1;
   }
}

Fields, Methods and Properties

This is a little counter-intuitive, but it is faster to access a property on an object than it is to access a field...