It seems maps in haxe are extremely slow vs dynamic objects
I would avoid them.
So using this code:
var nd=()->{
//var op:Dynamic = {x:100,y:1000};
//op.z = 22;
var op = {x:100,y:1000,z:22}
//var op = ['x'=>100,'y'=>1000];
//op['z'] = 22;
var i;
for(i in 0...1000000)
{
/*
op['x']++;
op['y']--;
op['z']++;
*/
op.x++;
op.y--;
op.z++;
}
trace('Line');
}
var j;
var q:Float = haxe.Timer.stamp();
for(j in 0...100) nd();
trace(haxe.Timer.stamp()-q);
It's amazing how slow maps are
It's not that maps are slow, it's that your test doesn't consider compiler optimizations. And it seems like it was ran in Debug mode?
Let's take a look at a slightly more verbose test (10x fewer iterations, shuffled order and averages):
import haxe.DynamicAccess;
class Main {
static inline var times = 10000;
static function testInline() {
var o = { x: 100, y: 1000, z: 22 };
for (_ in 0 ... times) {
o.x++;
o.y--;
o.z++;
}
}
static function getClass() {
return new Vector(100, 1000, 22);
}
static function testClass() {
var o = getClass();
for (_ in 0 ... times) {
o.x++;
o.y--;
o.z++;
}
}
static function testClassDynamic() {
var o:Dynamic = getClass();
for (_ in 0 ... times) {
o.x++;
o.y--;
o.z++;
}
}
static function getObj() {
return { x: 100, y: 1000, z: 22 };
}
static function testObj() {
var o = getObj();
for (_ in 0 ... times) {
o.x++;
o.y--;
o.z++;
}
}
static function testDynamic() {
var o:Dynamic = { x: 100, y: 1000, z: 22 };
for (_ in 0 ... times) {
o.x++;
o.y--;
o.z++;
}
}
static function testDynamicPlus() {
var o:Dynamic = { };
o.x = 100;
o.y = 1000;
o.z = 22;
for (_ in 0 ... times) {
o.x++;
o.y--;
o.z++;
}
}
static function testDynamicAccess() {
var o:DynamicAccess<Int> = getObj();
for (_ in 0 ... times) {
o["x"]++;
o["y"]--;
o["z"]++;
}
}
static function testMapString() {
var o = ["x" => 100, "y" => 1000, "z" => 22];
for (_ in 0 ... times) {
o["x"]++;
o["y"]--;
o["z"]++;
}
}
static function testMapInt() {
var o = [100 => 100, 200 => 1000, 300 => 22];
for (_ in 0 ... times) {
o[100]++;
o[200]--;
o[300]++;
}
}
static function shuffleSorter(a, b) {
return Math.random() > 0.5 ? 1 : -1;
}
static function main() {
var tests = [
new Test("inline", testInline),
new Test("class", testClass),
new Test("object", testObj),
new Test("object:Dynamic", testDynamic),
new Test("class:Dynamic", testClassDynamic),
new Test("object:Dynamic+", testDynamicPlus),
new Test("DynamicAccess", testDynamicAccess),
new Test("Map<String, Int>", testMapString),
new Test("Map<Int, Int>", testMapInt),
];
var shuffle = tests.copy();
var iterations = 0;
while (true) {
iterations += 1;
Sys.println("Step " + iterations);
for (i => v in shuffle) {
var k = Std.random(shuffle.length);
shuffle[i] = shuffle[k];
shuffle[k] = v;
}
for (test in shuffle) {
var t0 = haxe.Timer.stamp();
var fn = test.func;
for (_ in 0 ... 100) fn();
var t1 = haxe.Timer.stamp();
test.time += t1 - t0;
Sys.sleep(0.001);
}
for (test in tests) {
Sys.println('${test.name}: ${Math.ffloor(test.time / iterations * 10e6) / 1e3}ms avg');
}
Sys.sleep(1);
}
}
}
class Test {
public var time:Float = 0;
public var func:Void->Void;
public var name:String;
public function new(name:String, func:Void->Void) {
this.name = name;
this.func = func;
}
public function toString() return 'Test($name)';
}
class Vector {
public var x:Int;
public var y:Int;
public var z:Int;
public function new(x:Int, y:Int, z:Int) {
this.x = x;
this.y = y;
this.z = z;
}
}
And its output after a hundred or so "steps":
inline: 0.011ms avg
class: 15.737ms avg
object: 281.417ms avg
object:Dynamic: 275.509ms avg
class:Dynamic: 233.208ms avg
object:Dynamic+: 1208.83ms avg
DynamicAccess: 1021.248ms avg
Map<String, Int>: 1293.529ms avg
Map<Int, Int>: 916.552ms avg
Let's take a look at what each test compiles to.
Haxe-generated C++ code is formatted for readability
This is what you were testing, although you have evidently suspected something judging by the commented out line.
If it seems suspiciously fast, that's because it is - the Haxe compiler noticed that your object is local and inlined it completely:
void Main_obj::testInline()
{
HX_STACKFRAME(&_hx_pos_e47a9afac0942eb9_5_testInline)
int o_x = 100;
int o_y = 1000;
int o_z = 22;
{
int _g = 0;
while ((_g < 10000))
{
_g = (_g + 1);
int _ = (_g - 1);
o_x = (o_x + 1);
o_y = (o_y - 1);
o_z = (o_z + 1);
}
}
}
Consequently, the C++ compiler might figure out that you aren't really doing anything in this function, at which point the contents are removed:
(whereas if you were to return o.z
, the contents would be equivalent to return 10022
instead)
Let's talk about the things that you should be doing in a good case scenario.
Known field access on a class instance is very fast because it is compiled to a C++ class with direct field access:
::Vector Main_obj::getClass()
{
HX_GC_STACKFRAME(&_hx_pos_e47a9afac0942eb9_15_getClass)
return ::Vector_obj::__alloc(HX_CTX, 100, 1000, 22);
}
void Main_obj::testClass()
{
HX_STACKFRAME(&_hx_pos_e47a9afac0942eb9_17_testClass)
::Vector o = ::Main_obj::getClass();
{
int _g = 0;
while ((_g < 10000))
{
_g = (_g + 1);
int _ = (_g - 1);
o->x++;
o->y--;
o->z++;
}
}
}
Getting a class from a function call is required to prevent the Haxe compiler from inlining it; the C++ compiler may still collapse the for-loop.
Let's prevent the compiler from inlining the anonymous object by returning it from a function.
But it's still faster than a map.
Since dynamic objects are used pretty often (JSON and all), a few tricks are utilized - for instance, if you are creating an anonymous object with a set of predefined fields, additional work will be performed for these so that they can be accessed quicker (seen here as Create(n)
and a subsequent chain of setFixed
calls):
::Dynamic Main_obj::getObj()
{
HX_STACKFRAME(&_hx_pos_e47a9afac0942eb9_36_getObj)
return ::Dynamic(::hx::Anon_obj::Create(3)
->setFixed(0, HX_("x", 78, 00, 00, 00), 100)
->setFixed(1, HX_("y", 79, 00, 00, 00), 1000)
->setFixed(2, HX_("z", 7a, 00, 00, 00), 22));
}
void Main_obj::testObj()
{
HX_STACKFRAME(&_hx_pos_e47a9afac0942eb9_38_testObj)
::Dynamic o = ::Main_obj::getObj();
{
int _g = 0;
while ((_g < 10000))
{
_g = (_g + 1);
int _ = (_g - 1);
::hx::FieldRef((o).mPtr, HX_("x", 78, 00, 00, 00))++;
::hx::FieldRef((o).mPtr, HX_("y", 79, 00, 00, 00))--;
::hx::FieldRef((o).mPtr, HX_("z", 7a, 00, 00, 00))++;
}
}
}
You can see a handful of these tricks in Anon.cpp and Anon.h.
Same as above, but by typing the variable as Dynamic instead, and without an extra function call. Personally I wouldn't rely on this behaviour.
Although the code is effectively identical to above,
void Main_obj::testClassDynamic()
{
HX_STACKFRAME(&_hx_pos_e47a9afac0942eb9_26_testClassDynamic)
::Dynamic o = ::Main_obj::getClass();
{
int _g = 0;
while ((_g < 10000))
{
_g = (_g + 1);
int _ = (_g - 1);
::hx::FieldRef((o).mPtr, HX_("x", 78, 00, 00, 00))++;
::hx::FieldRef((o).mPtr, HX_("y", 79, 00, 00, 00))--;
::hx::FieldRef((o).mPtr, HX_("z", 7a, 00, 00, 00))++;
}
}
}
this runs a little faster. This is a accomplished by pre-generating functions for Reflection that will first check if the variable happens to be one of the predefined ones:
::hx::Val Vector_obj::__Field(const ::String &inName,::hx::PropertyAccess inCallProp)
{
switch(inName.length) {
case 1:
if (HX_FIELD_EQ(inName,"x") ) { return ::hx::Val( x ); }
if (HX_FIELD_EQ(inName,"y") ) { return ::hx::Val( y ); }
if (HX_FIELD_EQ(inName,"z") ) { return ::hx::Val( z ); }
}
return super::__Field(inName,inCallProp);
}
Same as Dynamic, but we're also forcing the runtime to jump through a few (unnecessary) hoops with Reflect functions.
void Main_obj::testDynamicAccess()
{
HX_STACKFRAME(&_hx_pos_e47a9afac0942eb9_66_testDynamicAccess)
::Dynamic o = ::Main_obj::getObj();
{
int _g = 0;
while ((_g < 10000))
{
_g = (_g + 1);
int _ = (_g - 1);
{
::String tmp = HX_("x", 78, 00, 00, 00);
{
int value = ((int)((::Reflect_obj::field(o, tmp) + 1)));
::Reflect_obj::setField(o, tmp, value);
}
}
// ...
}
}
}
We can disregard the aforementioned predefined field optimization by creating an empty Dynamic object and then filling it up with fields. This puts us pretty close to Map's performance.
void Main_obj::testDynamicPlus()
{
HX_STACKFRAME(&_hx_pos_e47a9afac0942eb9_55_testDynamicPlus)
::Dynamic o = ::Dynamic(::hx::Anon_obj::Create(0));
o->__SetField(HX_("x", 78, 00, 00, 00), 100, ::hx::paccDynamic);
o->__SetField(HX_("y", 79, 00, 00, 00), 1000, ::hx::paccDynamic);
o->__SetField(HX_("z", 7a, 00, 00, 00), 22, ::hx::paccDynamic);
{
int _g = 0;
while ((_g < 10000))
{
_g = (_g + 1);
int _ = (_g - 1);
::hx::FieldRef((o).mPtr, HX_("x", 78, 00, 00, 00))++;
::hx::FieldRef((o).mPtr, HX_("y", 79, 00, 00, 00))--;
::hx::FieldRef((o).mPtr, HX_("z", 7a, 00, 00, 00))++;
}
}
}
Given that Map is unable to benefit from most of the above contextual optimizations (in fact, many wouldn't make sense for normal use cases), its performance should not be particularly surprising.
void Main_obj::testMapString()
{
HX_GC_STACKFRAME(&_hx_pos_e47a9afac0942eb9_74_testMapString)
::haxe::ds::StringMap _g = ::haxe::ds::StringMap_obj::__alloc(HX_CTX);
_g->set(HX_("x", 78, 00, 00, 00), 100);
_g->set(HX_("y", 79, 00, 00, 00), 1000);
_g->set(HX_("z", 7a, 00, 00, 00), 22);
::haxe::ds::StringMap o = _g;
{
int _g1 = 0;
while ((_g1 < 10000))
{
_g1 = (_g1 + 1);
int _ = (_g1 - 1);
{
::String tmp = HX_("x", 78, 00, 00, 00);
{
int v = ((int)((o->get(tmp) + 1)));
o->set(tmp, v);
}
}
// ...
}
}
}
A bonus: to any or no surprise, computing a hash of an integer is cheaper than doing so for a string.
Don't be hasteful to write your code one or other way based on what a microbenchmark suggests. For example, although this might seem like an in-depth breakdown, it does not account for garbage collection nor optimization differences between various C++ compilers.