Skip to content

Curious performance difference between callbacks and promises #579

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

Closed
flimzy opened this issue Jan 28, 2017 · 4 comments
Closed

Curious performance difference between callbacks and promises #579

flimzy opened this issue Jan 28, 2017 · 4 comments

Comments

@flimzy
Copy link
Member

flimzy commented Jan 28, 2017

All of the benchmarks mentioned below, and a full description can be found here.

I'm experiencing some performance issues with my GopherJS PouchDB bindings, so this morning I wrote some benchmarks, and found that Promises seem to have 100~200x the overhead of callbacks:

BenchmarkDoPromise1         1000           1318000 ns/op
BenchmarkDoPromise2         2000           1253000 ns/op
BenchmarkDoPromise3         1000           2358000 ns/op
BenchmarkDoCallback1      200000              9715 ns/op
BenchmarkDoCallback2      200000              6280 ns/op

In the spirit of @r-l-x's benchmarks on PR #558, I added two more tests, to benchmark only the channel operations:

func BenchmarkRawPromise(b *testing.B) {
	ch := make(chan promiseResult)
	defer close(ch)
	b.ResetTimer()
	b.StopTimer()
	for i := 0; i < b.N; i++ {
		js.Global.Call("EmptyPromise").Call("then", func(r *js.Object) {
			b.StartTimer()
			ch <- promiseResult{result: r.String()}
		})
		<-ch
		b.StopTimer()
	}
}

func BenchmarkRawCalback(b *testing.B) {
	ch := make(chan promiseResult, 1)
	defer close(ch)
	b.ResetTimer()
	b.StopTimer()
	for i := 0; i < b.N; i++ {
		js.Global.Call("EmptyCallback", func(r *js.Object) {
			b.StartTimer()
			ch <- promiseResult{result: r.String()}
		})
		<-ch
		b.StopTimer()
	}
}

Of particular note, the code between b.StartTimer() and b.StopTimer() is identical in both tests:

			b.StartTimer()
			ch <- promiseResult{result: r.String()}
		})
		<-ch
		b.StopTimer()

Yet the results are drastically different (by a factor of ~200x):

BenchmarkRawPromise         2000           1156000 ns/op
BenchmarkRawCalback       200000              6070 ns/op

So I then added BenchmarkChannel, and BenchmarkChannelInGoroutine:

func BenchmarkChannel(b *testing.B) {
	ch := make(chan int, 1)
	defer close(ch)
	b.ResetTimer()
	b.StopTimer()
	for i := 0; i < b.N; i++ {
		b.StartTimer()
		ch <- 0
		<-ch
		b.StopTimer()
	}
}

func BenchmarkChannelInGoroutine(b *testing.B) {
	ch := make(chan int, 1)
	defer close(ch)
	b.ResetTimer()
	b.StopTimer()
	for i := 0; i < b.N; i++ {
		go func() {
			b.StartTimer()
			ch <- 0
		}()
		<-ch
		b.StopTimer()
	}
}

With the following results:

BenchmarkChannel                  500000              2968 ns/op
BenchmarkChannelInGoroutine       300000              5056 ns/op

This seems intuitive to me, and aligns with the result of BenchmarkRawCallback. So why the drastic difference with Promises?

What am I overlooking? Is there a reason to expect this disparity?

@flimzy
Copy link
Member Author

flimzy commented Feb 5, 2017

After seeing @neelance's comment today about his scheduler branch, I re-ran these benchmarks against that branch, and saw apparent improvements for callbacks, and drastic improvements for promises!

BenchmarkChannel                  500000              2594 ns/op
BenchmarkChannelInGoroutine       300000              4700 ns/op
BenchmarkRawPromise               200000              7260 ns/op
BenchmarkRawCalback               300000              4470 ns/op
BenchmarkDoPromise1               100000             20520 ns/op
BenchmarkDoPromise2               100000             15280 ns/op
BenchmarkDoPromise3                 1000           1238000 ns/op
BenchmarkDoCallback1              200000              8805 ns/op
BenchmarkDoCallback2              200000              5565 ns/op

Promises are still a bit slower than callbacks, but now the difference is small enough it seems reasonable to assume it's just the difference in JS overhead.

I don't know why it makes such a difference; I don't see anything in the commits on that branch that to my reading should relate specifically to promises, but it's encouraging to me nonetheless.

@neelance
Copy link
Member

neelance commented Feb 5, 2017

The main difference is that I eliminated setTimeout($runScheduled, 0) from the JS-to-Go code path. The Go scheduler is now directly executed by the JS callback.

I'm happy to hear that it also improved the benchmarks here. Does everything else work fine for you with that branch? Could we close this issue after merging?

@flimzy
Copy link
Member Author

flimzy commented Feb 5, 2017

I've been using the scheduler branch for my regular development/testing work today (~3 hours of dedicated human time), and no problems so far in either Chrome or node. 👍

I see no reason to leave this issue open once that's merged.

@neelance
Copy link
Member

neelance commented Feb 5, 2017

Sounds good. Merged.

@neelance neelance closed this as completed Feb 5, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants