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);
- 地图:19 秒
- 动态对象:9 秒
- 对象:0.6 秒
地图的速度之慢令人惊讶
不是地图慢,而是你的测试没有考虑编译器优化。似乎是 运行 调试模式?
让我们来看一个稍微冗长的测试(迭代次数减少 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++ 编译器之间的优化差异。
似乎 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);
- 地图:19 秒
- 动态对象:9 秒
- 对象:0.6 秒
地图的速度之慢令人惊讶
不是地图慢,而是你的测试没有考虑编译器优化。似乎是 运行 调试模式?
让我们来看一个稍微冗长的测试(迭代次数减少 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++ 编译器之间的优化差异。