Optimizing Jint part 4: Interfaces are good, virtual calls better, direct field access the best

I'm probably going to break all the rules of OO encapsulation and some SOLID principles here. Be warned.

We all have had our share of the strategy pattern, abstract factory, you name it. Abstracting targets behind interfaces and abstract base classes. They all are fine and aim to produce better software and they usually do. But when it comes to performance, you should sometime bend the rules a bit.

Is it library code or your code?

If you are writing a library, you usually aim to create abstraction and interfaces that give you some room to move later on. You want to allow plugin points and all the shenanigans, they tend to make your library more usable and lessen the need of those pesky PRs that want to change the thing to do something it wasn't originally mean to do.

But, if you are writing code that is consumed by your team or does not have public API otherwise, you should consider what kind of types you want to take in and use.

Virtual methods and interfaces in Jint

Jint had places where less concrete type was used, like private field holding interface type for collection instead of concrete type, those were changed to be more concrete. Some hot properties were changed to be accessed directly via field when profiler showed percentages were used to call the getter, which is a method after all. It all adds up.

How does the performance differ

Here you can see five different cases:
  • SumHideous - uses old-school loop using mutating variable and indexes directly, ugh!
  • SumConcreteList - foreach over concrete type
  • SumConcreteEnumerable - using IEnumerable to loop and calling concrete type
  • SumInterfaceList - foreach calling property via interface
  • SumInterfaceEnumerable - using IEnumerable to loop and calling interface member
And the results? You can see that foreach is smart enough to use struct-based enumerator to rid of allocations, but otherwise the numbers are revealing. Had you used IList instead of List as variable type compiler would have been forced to box a enumerator. The more you add abstractions, the slower it gets. It all adds up.

And remember, I'm always referring to the tight loops and critical parts of your system. Do what you will with your less used, hardly required functionalities, until production starts to crash 😏

Loops in Jint

Being an interpreter, Jint does a lot of looping. The syntax tree is interpreted by looping and recursing, this what makes fast loop consruction a must. During optimization of Jint most of the loops were changed to be plain old for loops or foreach could be kept in case of concrete arrays or dictionaries.

Things to remember and consider

Some rules of thumb I tend to follow nowadays when it comes to hot paths where performance matters:
  • For loop is always a solid choice, with concrete arrays foreach will perform the same
  • Fields should always be private/internal and of concrete type
    • Only resort to direct field acces to items you don't own when absolutely necessary and you've done the benchmarks to justify it
  • Interfaces have a lot of penalty, consider virtual methods instead
  • Virtual methods have their price, don't mark methods virtual before you need to
    • Marking a class sealed will signal virtual machine that less work can be done when checking the virtual method table while doing the dynamic dispatch
  • Return and take concrete types from/to internal/private methods
    • R# often suggests to replace List with even IEnumerable when it's only used for foreach, but do consider the performance effect
  • Expose most usable and yet abstract type that makes sense
    • IReadOnlyList is a nice example that tells that go ahead and use indexer, it might be my private parts so don't change anything, I might have saved some memory by not creating a new instance

Comments

Popular posts from this blog

Optimizing Jint part 6: DictionarySlim is coming to town

Running docker-compose against WSL2