Haxe Maps vs 动态对象 vs 固定对象性能 CPP

Haxe Maps vs Dynamic Object vs Fixed Object performance CPP

似乎 haxe 中的地图与动态对象相比非常慢

我会避开它们。

因此使用此代码:

        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);

地图的速度之慢令人惊讶

不是地图慢,而是你的测试没有考虑编译器优化。似乎是 运行 调试模式?

让我们来看一个稍微冗长的测试(迭代次数减少 10 倍,打乱顺序和平均值):

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;
    }
}

一百个左右“步骤”后的输出:

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

让我们看看每个测试编译成什么。
Haxe 生成的 C++ 代码经过格式化以提高可读性

内联

这就是您正在测试的内容,尽管根据注释掉的行您显然怀疑有什么问题。

如果它看起来快得可疑,那是因为它是 - Haxe 编译器注意到您的对象是本地对象并完全内联它:

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);
        }
    }
}

因此,C++ 编译器可能会发现您没有真正在此函数中执行任何操作,此时内容将被删除:

(而如果您要 return o.z,则内容将等同于 return 10022

class

让我们谈谈在一个好的案例场景中你应该做的事情。

class 实例上的已知字段访问非常快,因为它被编译为具有直接字段访问的 C++ class:

::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++;
        }
    }
}

需要从函数调用中获取 class 以防止 Haxe 编译器内联它; C++ 编译器可能仍然会崩溃 for 循环。

对象

让我们通过从函数返回匿名对象来阻止编译器内联它。

但它仍然比地图快。

由于经常使用动态对象(JSON 和所有),因此使用了一些技巧 - 例如,如果您正在创建具有一组预定义字段的匿名对象,则会执行额外的工作对于这些,以便可以更快地访问它们(此处显示为 Create(n) 和随后的 setFixed 调用链):

::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))++;
        }
    }
}

您可以在 Anon.cpp and Anon.h 中看到其中的一些技巧。

动态

与上面相同,但将变量键入为 Dynamic 而不是额外的函数调用。我个人不会依赖这种行为。

class:动态

尽管代码实际上与上面相同,

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))++;
        }
    }
}

这运行得快一点。这是通过为反射预生成函数来实现的,该函数将首先检查变量是否恰好是预定义变量之一:

::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);
}

动态访问

与 Dynamic 相同,但我们还强制运行时使用 Reflect 函数跳过一些(不必要的)环节。

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);
                }
            }
            // ...
        }
    }
}

对象:动态+

我们可以忽略前面提到的预定义字段优化,方法是创建一个空的 Dynamic 对象,然后用字段填充它。这使我们非常接近 Map 的性能。

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))++;
        }
    }
}

Map

鉴于 Map 无法从上述大多数上下文优化中获益(事实上,许多对于正常用例来说没有意义),它的性能应该不会特别令人惊讶。

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);
                }
            }
            // ...
        }
    }
}

Map

好处:无论是否令人惊讶,计算整数的散列比对字符串计算散列的成本更低。

结论

不要急于根据微基准测试的建议以一种或另一种方式编写代码。例如,虽然这看起来像是一个深入的细分,但它没有考虑垃圾收集,也没有考虑各种 C++ 编译器之间的优化差异。