Performance hit of reflection (specifically Std.is)

I’m thinking to use a logging library (currently looking at hexLog). I’ve seen that the library makes use of Std.is and I’m curious how much this may impact the performance of a JS output if my application makes use of a lot of logging.

I haven’t looked in the JS output yet and I don’t know how Haxe transpiles reflection features into JS (also, my JS is a bit rusty ATM; planning to brush up after I’m done with this game prototype I’m working on).

Haxe tries to optimize Std.is calls, especially on JS, but in general it’s a good idea to avoid downcasting and reflection in favor of better architecture and/or compile-time features :smiley:

3 Likes

Thank you @nadako :). I agree. I just recently avoided Std.is by making better use of overloading methods.

One more question: what about Type.getClassName(Type.getClass(this)).

Seems to me like this one shouldn’t be that costly? I’m only calling this in the constructor, so I can initialize a logger with a unique name.

I’m open to suggestions as always :slight_smile:

Yeah that’s not too expensive of course. In Haxe 4 Type.getClassName(cls) is just cls.__name__ on JS. Type.getClass is a bit heavier as it has to deal with some native JS classes, but essentially it’ll check for inst.__class__ and return it. So should be totally fine to call in the initialization!


As an alternative, if the classes are always known at compile-time and you just want to avoid string literals, you can write a simple macro to translate a class argument to a string, like this:

import haxe.macro.Context;
import haxe.macro.Expr;

class Logger {
	public static macro function getLoggerByClass(cls:ExprOf<Class<Dynamic>>):Expr {
		var name = switch Context.typeExpr(cls).expr {
			case TTypeExpr(TClassDecl(c)): c.toString();
			case _: throw new Error("Class expected", cls.pos);
		}
		return macro Logger.getLoggerByName($v{name});
	}

	public static function getLoggerByName(name:String):Logger {
		return null;
	}
}

then if you do e.g. var logger = Logger.getLoggerByClass(haxe.Json);, the actual generated code will be var logger = Logger.getLoggerByName("haxe.Json");

Not saying that you should do it, because really there’s no need to overcomplicate this simple task when you don’t have any performance bottleneck, but just so you know it’s totally possible. :slight_smile:

Avoid reflection. It just bloats the output and is more of a legacy thing in Haxe, which has pretty much evolved beyond a need for it. Consider this simple logging utility for js:

import js.Browser.console;

@:enum abstract LogLevel(Int) to Int {
  var Debug = 0;
  var Info = 1;
  var Warning = 2;
  var Error = 3;
}
class Log {
  static inline var LOG_LEVEL = 
    #if (log_level == "info") 1
    #elseif (log_level == "warning") 2
    #elseif (log_level == "error") 3
    #else 0
    #end
  ;
  static public inline function debug(msg:Dynamic, ?pos:haxe.PosInfos)
    if (LOG_LEVEL <= 0) log(Debug, msg, pos);
    
  static public inline function info(msg:Dynamic, ?pos:haxe.PosInfos)
    if (LOG_LEVEL <= 1) log(Info, msg, pos);
    
  static public inline function warning(msg:Dynamic, ?pos:haxe.PosInfos)
    if (LOG_LEVEL <= 2) log(Warning, msg, pos);
    
  static public inline function error(msg:Dynamic, ?pos:haxe.PosInfos)
    log(Error, msg, pos);
        
  @:noCompletion 
  static public dynamic function log(level:LogLevel, msg:Dynamic, ?pos:haxe.PosInfos) {
    var out = switch level {
      case Debug: console.debug;
      case Info: console.info;
      case Warning: console.warn;
      case Error: console.error;
    }
    out('${pos.fileName}@${pos.lineNumber}', msg);
  }
}

Adjusting the log level via -D log_level=warning will lead to all Log.info and Log.debug not even being generated in the output code. The Log.log function can be reassigned to something clever that filters on the position information, sends the data to the server etc. etc. None of this requires reflection.

This simplistic example can be improved in countless ways, for example:

  • you can turn all these calls into macros and then filter based on call position, environment variables and what not at compile time

  • you can make the functions return the expression being logged, e.g.:

    static public inline function warning<T>(msg:T, ?pos:haxe.PosInfos) {
      if (LOG_LEVEL <= 2) log(Warning, msg, pos);
      return T;
    }
    

    This way you can just insert log statements, without having to restructure your code.

  • anything else you might come up with, except adding reflection :wink:

4 Likes

Just for the sake of completeness, hexLog also has a set of convenience macros if you want to avoid reflection. It does similar things to what @nadako described and replaces the getLogger calls with simple strings. GitHub - DoclerLabs/hexLog: Logging system inspired by log4j written in Haxe

3 Likes

Wow, excellent answers and guidance. Thank you so much! :slight_smile:

This is a situation where I can seriously improve my Haxe skills just by executing the changes indicated by you.

@back2dos I am curious why you think that reflection is a “legacy” thing and not required. I do try to stay away from it, but here’s a situation I just experienced where I can’t figure out a good way to get rid of it:

I’m building a filtering system that involves, among others, two very different filters. Hierarchical (similar to let’s say a package structure or log4j) and Array-based (similar to let’s say the way you set your log levels in log4j). Ok so these two filters have a common ancestor, but not much else can be shared. Both filters implement an interface FilterI that ensures they have a match function. But the logic of the match function is of course very different between the two as they have different data structure. The problem appears when I’m trying to match a filter of one type against another one (a match results in a “find”).

I just typed another 2 long paragraphs to explain the issue to you and just as I was almost done, I realized “oh wait, I can declare an enum in the interface and each filter will set itself as a member of the enum and then I’ll just match against the enum in the match function problem solved closing quote”.

:smiley:

But seriously, why do you say it’s a legacy thing?

I don’t think Reflection is a legacy thing, there are some things you can’t accomplish without it, such as an interpreter like hscript. That can be used to power cool things like “runtime auto-completion”. Of course, that’s more of a debugging feature.

But yeah, in most cases Reflection can probably be replaced by macros.

That’s pretty much what I think about runtime reflection. What makes it so powerful and thus popular is the fact that you can write code that dynamically responds to the structure of data, by inspecting it at runtime. Via macros, such inspection is possible at compile time and you can then generate type aware code from there. Such code is safer (it gets type checked), faster at runtime and increases code size proportionally to the size of the structure of the data it processes, while using reflection add runtime type information for all code, even the parts where it’s not used. E.g.: use Type.getClassName on a single class, and all classes will have their names added to the output. The compiler cannot really be any smarter about this, because by its very nature runtime reflection is hard to statically analyze.

There are some extreme cases where reflection makes for a decent choice. As Jens pointed out, hscript cannot do without it. It’s equally hard to implement an interpreter without unstructured jumps and if you inspect hscript’s source, you’re sure to find them.

Still, if you come across a problem that requires transferring flow control via an unstructured jump, my advice to you would be to look really hard if you can’t approach things from a different angle. As far as Haxe is concerned, I would say the same about reflection. Since Haxe 3 (and to some extent even prior to that) it can be avoided. It’s harder (especially if you haven’t made it a habit yet), but it pays off for any sufficiently long running project, because type safe code is more easily refactored … and beyond being faster and smaller at runtime, it’s more easily ported, so if you choose to expand to another platform you get another return on your investment.

7 Likes

FYI, the efficacy of reflection is also influenced by the target.

  • With a target like JS or PHP, a “variable” or “a piece of data” is a self-describing object, with information available at run time that is in fact evaluated at run time.

  • Whereas, a target like C++ has the compiler doing the work ahead of time, and data is usually not dynamic.

  • Yes, sometimes you do have to resort to reflection to get the job done, but, when you do, you definitely want to encapsulate the code that does so … to preserve Haxe’s compile-time-error capabilities as strongly as possible.

As others have said, if you find yourself reaching for reflection, there’s probably a better and more portable way to accomplish the same goal. In particular, reflection forces the evaluation process to "run- time" … “Holy JavaScript, Batman!” … and this really isn’t where you want such things to be. (“No, it isn’t simply a matter of ‘performance hits,’ IMHO.”)

1 Like