Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Inline Array.map/filter methods #5749

Merged
merged 17 commits into from
Aug 21, 2019

Conversation

nadako
Copy link
Member

@nadako nadako commented Oct 13, 2016

As we discussed in Slack, it would be nice to inline array map and filter methods so we can avoid function calls and closure creation when we use anonymous functions.

With this, if we write a.map(function(i) return i * 2), we get:

C++

HXLINE(   6)        HX_VARI( ::Array< Int >,result) = ::Array_obj< Int >::__new(0);
HXDLIN(   6)        _hx_array_set_size_exact(result,a->length);
HXDLIN(   6)        {
HXLINE(   6)            HX_VARI( Int,_g1) = (int)0;
HXDLIN(   6)            HX_VARI( Int,_g) = a->length;
HXDLIN(   6)            while((_g1 < _g)){
HXLINE(   6)                _g1 = (_g1 + (int)1);
HXDLIN(   6)                HX_VARI( Int,i) = (_g1 - (int)1);
HXDLIN(   6)                {
HXLINE(   6)                    Int inValue = (( (Int)(_hx_array_unsafe_get(a,i)) ) * (int)2);
HXDLIN(   6)                    result->__unsafe_set(i,inValue);
                            }
                        }
                    }

JS:

    var a1 = new Array(a.length);
    var _g1 = 0;
    var _g = a.length;
    while(_g1 < _g) {
        var i = _g1++;
        a1[i] = a[i] * 2;
    }

It's similar in other targets (also note the preallocation optimization for map). And it still works through Dynamic/structure typing, because actual Array objects have these methods as well (they are marked with @:runtime for extern Array's so they really work).

In Java I had to use __get/__set methods that do bounds checking instead of faster __unsafe_get/set because of #5748. Let's change that when it's fixed.

@nadako
Copy link
Member Author

nadako commented Oct 13, 2016

Oops, C# fails at the moment, partly due to #5747

@Simn
Copy link
Member

Simn commented Mar 20, 2017

What's the state here?

@nadako
Copy link
Member Author

nadako commented Mar 20, 2017

Oh, I have to check.

@nadako nadako force-pushed the array_map_inline branch from b242d69 to 25d9087 Compare March 20, 2017 22:18
@RealyUniqueName
Copy link
Member

RealyUniqueName commented Mar 20, 2017

map is already inlined for php7. filter could be reworked to completely eliminate function calls.

@RealyUniqueName
Copy link
Member

Up :)

@nadako nadako force-pushed the array_map_inline branch from 25d9087 to 7849a33 Compare May 1, 2017 22:02
@nadako
Copy link
Member Author

nadako commented May 1, 2017

Rebased. C# tests still fail with an issue that looks like #5748

@nadako nadako force-pushed the array_map_inline branch from 7849a33 to 59a575a Compare June 18, 2017 09:19
@RealyUniqueName
Copy link
Member

This PR is affected by 11186ef. Anonymous functions are not inlined anymore.

@Simn Simn added this to the Design milestone Apr 20, 2018
@Simn
Copy link
Member

Simn commented Sep 3, 2018

This would need an update.

@Simn
Copy link
Member

Simn commented Sep 8, 2018

This is blocked on #5748 (and on @nadako updating it).

@RealyUniqueName
Copy link
Member

Updated. Let's do this for 4.0

@RealyUniqueName
Copy link
Member

Blocked by #8654

@RealyUniqueName
Copy link
Member

Ready to go

@RealyUniqueName RealyUniqueName merged commit 6a44de1 into HaxeFoundation:development Aug 21, 2019
return ret;
}

public function filter(f:T->Bool):Array<T> {
public inline function filter( f : T -> Bool ) : Array<T> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Strange spacing here.

@nadako nadako deleted the array_map_inline branch August 21, 2019 17:39
@jcward
Copy link
Contributor

jcward commented Aug 21, 2019

Awesome!

For the curious like me, the CPP and JS output of the OP example -- prior to this merge -- generates closures, something like:

C++:

            	HX_BEGIN_LOCAL_FUNC_S0(hx::LocalFunc,_hx_Closure_0) HXARGC(1)
            	int _hx_run(int i){
            		HX_STACKFRAME(&_hx_pos_df13cafffbed243f_6_main)
HXLINE(   6)		return (i * 2);
            	}
            	HX_END_LOCAL_FUNC1(return)

            	HX_STACKFRAME(&_hx_pos_df13cafffbed243f_3_main)
HXLINE(   6)		a->map( ::Dynamic(new _hx_Closure_0()));
            	}

JS:

	var a = [1,2,3];
	var b = a.map(function(i) {
		return i * 2;
	});

I suspect this won't affect node / chrome / V8 drastically, but CPP (and HL?) could see a nice gain.

@RealyUniqueName
Copy link
Member

RealyUniqueName commented Aug 21, 2019

Actually it makes huge difference even on node:

import haxe.Timer;

class Main {
	public static function main():Void {
		var a = [for(i in 0...0xFFF) i];
		var b;
		Timer.measure(() -> for(i in 0...0xFFF) b = a.map(i -> i));
		// Timer.measure(() -> for(i in 0...0xFFFF) b = js.Syntax.code('{0}.map({1})', a, i -> i));
	}
}

The first line result (with this patch in effect)

$ haxe -main Main -js test.js && node test.js
Main.hx:7: 0.0690000057220459s

The second line result (without this patch)

$ haxe -main Main -js test.js && node test.js
Main.hx:8: 0.7279999256134033s

Edit:
Sorry, because of a typo the first line runs for 0xFFF iterations while the second line runs for 0xFFFF iterations.
With the same amount of iterations 0xFFFF the first line (with this patch) is even worse than no inlining:

$ haxe -main Main -js test.js && node test.js
Main.hx:7: 1.0940001010894775s

@RealyUniqueName
Copy link
Member

RealyUniqueName commented Aug 21, 2019

Should I disable inlining for js?

@RealyUniqueName
Copy link
Member

Though for a single map call this patch is almost nine times faster.

@jcward
Copy link
Contributor

jcward commented Aug 21, 2019

Ok, I stand corrected! Many performance tweaks I've played with have little affect on V8 (presumably they've invested heavily in optimizations). I expected they'd be able to optimize away the inner function call, but I'm surprised not:

https://jsperf.com/inlining-map-function shows 5X faster in my Chrome 73 browser:

image

Interestingly, when I run basically the same test in node, I'm seeing it 26X faster:

>node inline_map_test.js
   Loop: avg 4.28 ms
Closure: avg 112.47 ms

I don't have this patch yet, but I'm interesting in CPP and HL.

@RealyUniqueName
Copy link
Member

Ah, that's because we create new array like [] instead of new Array(length). But new Array(length) comes with slower item access afterwards: #8040 (comment)

@RealyUniqueName
Copy link
Member

I wonder if creating an array with new Array(length) and then returning a.copy() would gain benefits of both sides. Maybe V8 could figure out the type of items in cloned array in this case.

RealyUniqueName added a commit that referenced this pull request Aug 22, 2019
@RealyUniqueName
Copy link
Member

RealyUniqueName commented Aug 22, 2019

Maybe V8 could figure out the type of items in cloned array in this case.

Checked. It can't.

I've pushed array creation via new Array(length) in map. Because "native" Array.map() creates arrays with slow array access too.
So if one needs a lot of array mapping on js target, then Array.map() is the way to go. But if one needs faster array access, then var mapped = [for(i in original) func(i)]; is better.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants