-
Notifications
You must be signed in to change notification settings - Fork 18
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
[WIP] optimisation experiments #65
Conversation
Actually I can't see any noticeable improvement in the benchmark with that change. |
I think actually generating an optimized code for each datatype would be interesting. edit: I mean mostly the extended types. |
Currently testing with: var Benchmark = require('benchmark');
var ProtoDef = require("protodef").ProtoDef;
var readi8=require("./dist/datatypes/numeric")["i8"][0];
var proto=new ProtoDef();
var buf=new Buffer([0x3d]);
//var v=proto.types.i8[0];
var suite = new Benchmark.Suite;
suite
.add('readInt8', function() {
buf.readInt8(0);
})
.add('protodef read i8', function() {
proto.read(buf,0,"i8");
})
.add('protodef parsePacketBuffer i8', function() {
proto.parsePacketBuffer("i8",buf);
})
.add('protodef read i8 raw', function() {
readi8(buf,0);
})
.add('protodef read i8 types', function() {
proto.readI8(buf,0);
//v(buf,0);
})
.on('cycle', function(event) {
console.log(String(event.target));
})
.run({ 'async': false }); See #28 for details. |
21aa8bf
to
950e6b6
Compare
Next: optimizing containers Bench: var Benchmark = require('benchmark');
var ProtoDef = require("protodef").ProtoDef;
var proto=new ProtoDef();
var buf=new Buffer([0x1d,0x2d,0x3d,0x4d]);
var suite = new Benchmark.Suite;
suite
.add('readContainer', function() {
return {
value: {
a: buf.readInt8(0),
b: buf.readInt8(1),
c: buf.readInt8(2),
d: buf.readInt8(3)
},
size:4
};
})
.add('protodef readContainer', function() {
proto.readContainer(buf,0,[
{
"name":"a",
"type":"i8"
},
{
"name":"b",
"type":"i8"
},
{
"name":"c",
"type":"i8"
},
{
"name":"d",
"type":"i8"
}
],{});
})
.on('cycle', function(event) {
console.log(String(event.target));
})
.run({ 'async': false }); Current results:
Pretty bad. Plan: doing addType and remove the need to pass typeArgs around. (see #28 (comment) ) |
var Benchmark = require('benchmark');
var ProtoDef = require("protodef").ProtoDef;
var proto=new ProtoDef();
var buf=new Buffer([0x1d,0x2d,0x3d,0x4d]);
var suite = new Benchmark.Suite;
proto.addType("myContainer",["container",[
{
"name":"a",
"type":"i8"
},
{
"name":"b",
"type":"i8"
},
{
"name":"c",
"type":"i8"
},
{
"name":"d",
"type":"i8"
}
]]);
suite
.add('readContainer', function() {
return {
value: {
a: buf.readInt8(0),
b: buf.readInt8(1),
c: buf.readInt8(2),
d: buf.readInt8(3)
},
size:4
};
})
.add('protodef readContainer', function() {
proto.readMyContainer(buf,0,{},{});
})
.on('cycle', function(event) {
console.log(String(event.target));
})
.run({ 'async': false }); Current perf by adding a custom type are even worse:
|
function myCustomContainer(buffer, offset, typeArgs, context){
var size=0;
var value={};
var result;
result=proto.readI8(buffer,offset+size);
value["a"]=result.value;
size+=result.size;
result=proto.readI8(buffer,offset+size);
value["b"]=result.value;
size+=result.size;
result=proto.readI8(buffer,offset+size);
value["c"]=result.value;
size+=result.size;
result=proto.readI8(buffer,offset+size);
value["d"]=result.value;
size+=result.size;
return {
value:value,
size:size
}
} is really fast. Even faster than the baseline for some reason. (30M ops/sec) Edit: ah the reason why it was faster is because I didn't enable the noAssert option in the baseline. Anyway, pretty good, now to generate this. |
function containerReaderBuild(typeArgs)
{
var s="function o(proto){";
s+="return function generated_(buffer, offset){\n";
s+="var size=0;\n";
s+="var value={};\n";
s+="var result;\n";
typeArgs.forEach(o => {
s+="result=proto.read"+capitalizeFirstLetter(o.type)+"(buffer,offset+size);\n";
s+='value["'+o.name+'"]=result.value;'+"\n";
s+='size+=result.size;'+"\n";
});
s+="return {value:value,size:size}\n";
s+="}\n";
s+="}\n";
s+="o;";
return eval(s);
}
var generated=containerReaderBuild(types)(proto);
console.log(generated(buf,0)); works nicely:
|
Apparently http://sweetjs.org/ is a good lib for generating js. |
Ah something I learned here : using this. or using eval with a global variable in the code makes things slow. (that's why I have that |
Sweetjs is a great idea, but I'm having a hard time understanding how to implement it. Oh and here's a rewritten version using es6 goodies :D const containerReaderBuild = (typeArgs) => eval(`(function (proto) {
function generate(buffer, offset) {
var size=0;
var value={};
var result;
${typeArgs.reduce((old, o) => old + `
result = proto.read${capitalizeFirstLetter(o.type)}(buffer, offset + size);
value['${o.name}'] = result.value;
size += result.size;
`, "")}
return {value:value,size:size};
}
});`);
const generated = containerReaderBuild(types)(proto);
console.log(generated(buf,0)); |
Yeah using that es6 syntax makes that code look much better. Edit: I don't know how exactly would sweetjs be used here either, it's just something that's been advised to me. I might try to figure that out later. |
Adding back the context destroys the perf. (/20) Got to figure out a way to add it back and keep good perf. |
This is only 2 times slower than the manual version (that doesn't have context handling !): const containerReaderBuild = (typeArgs) => eval(
`((proto) => {
return (buffer, offset, context) => {
var size=0;
var value={};
var value2={};
value[".."]=context;
var result;
${typeArgs.reduce((old, o) => old + `
result = proto.read${capitalizeFirstLetter(o.type)}(buffer, offset + size);
${o.anon
? `if(result.value !== undefined)
Object.keys(result.value).forEach(key => value2[key]=value[key] = result[key]);`
: `value2['${o.name}'] = value['${o.name}'] = result.value;`
}
size += result.size;
`, "")}
return {value:value2,size:size};
}
});`);
const generated = containerReaderBuild(types)(proto);
console.log(generated(buf,0)); I guess it can be ok. |
I might consider trying to predict at compile time whether context handling is necessary or not. (It's possible if some meta information is added with each type : for example numerical types don't need context) |
No need for meta information : you can get the number of argument a function takes by using Function.prototype.length. For instance, numeric.readInt.length should return 3, whereas structures.containers.length should return 5. This should be enough to guess whether context is needed or not. |
Oh yeah indeed. |
Concerning context, what we have right now is a poorly obtimized tree in the form of nested hash tables. That obviously sucks performance wise. It's also a terrible over-engineering on my end. Thinking back, there is a simple and elegant solution that should be implementable. The idea is to only one context object created at read. Instead of creating a new context, readContainer(buffer, offset, typeArgs, context, ns) {
typeArgs.forEach(({type,name,anon}) => {
tryDoc(() => {
var readResults = this.read(buffer, offset, type, context, ns + "." + name);
results.size += readResults.size;
offset += readResults.size;
results.value[name] = readResults.value;
}, name ? name : "unknown");
});
return results;
} This design isn't ideal because it adds an additional arg. I'm thinking maybe using es6 "Map" and having those who need to add a namespace somehow override their prototype or somth. But the basic idea is there. |
How can this work ? The this.read called need the values read in that container: they need to be added to the context. |
fail. |
Yeah but that's not my problem. What does context contain? It's currently filled in readContainer and used in stuff called by readContainer (for example the compareTo of switch or the countBy of array). |
This is why you shouldn't code at 3am. readContainer(buffer, offset, typeArgs, context, ns) {
typeArgs.forEach(({type,name,anon}) => {
tryDoc(() => {
var readResults = this.read(buffer, offset, type, context, `${ns}.${name}`);
results.size += readResults.size;
offset += readResults.size;
results.value[name] = readResults.value;
context[`${ns}.${name}`] = readResults.value;
}, name ? name : "unknown");
});
return results;
} this does introduce some complexity with regards to |
Well looking back, context is implemented as a hack in readContainer (basically hijacking the return result) that should be fairly optimal. Durr 😅 . So I was wondering, what exactly did you add back to destroy the perfs ? The |
yes Having a second object to store the results, which doesn't contain ".." and isn't passed around in the Indeed doing |
Getting the same result as the manual one with: const containerReaderBuild = (typeArgs) => {
const requireContext=typeArgs.filter(o => proto[`read${capitalizeFirstLetter(o.type)}`].length==3).length>0;
return eval(`((proto) => {
return (buffer, offset${requireContext ? `,context`:``}) => {
var size=0;
var value2={};
${requireContext ? `
var value={};
value[".."]=context;
` :``}
var result;
${typeArgs.reduce((old, o) => old + `
result = proto.read${capitalizeFirstLetter(o.type)}(buffer, offset + size${requireContext ? `,value`:``});
${o.anon
? `if(result.value !== undefined)
Object.keys(result.value).forEach(key => ${requireContext ? `value[key]=` : ``}value2[key] = result[key]);`
: `${requireContext ? `value['${o.name}'] =` : ``} value2['${o.name}'] = result.value;`
}
size += result.size;
`, "")}
return {value:value2,size:size};
}
});`);
};
const generated = containerReaderBuild(types)(proto);
console.log(generated(buf,0));
|
It's not very pretty but I think that's not really a problem, and the code look can be improved if needed. |
interesting here : https://github.com/mafintosh/is-my-json-valid is the second fastest json schema validator and it also uses code generation. The way it generates js might be interesting |
For example https://www.npmjs.com/package/generate-function which https://www.npmjs.com/package/protocol-buffers uses. |
An idea : the current master is a protodef format interpreter. What I'm trying to build here is a protodef format compiler. It might be interesting to keep both (in separate repos I guess). So the interpreter code is more readable and it's possible to check an interpretation against it more easily. |
apparently mineflayer tends to consome 20% cpu of big servers, and having a proxy with 20 clients tends to get lagging. I'll try to advance on this so everything using protodef can be faster. |
This reverts commit 085d509.
…0x improvement) numerical datatypes, add read_<packet> functions as part of that optimization
950e6b6
to
a774cb4
Compare
https://gist.github.com/hansihe/b5b1754e2447c50680fc example of code produced by elixir-protodef |
consider generating a single function for a given type to eliminate function calls completely and also eliminate the context object. |
maybe I should just start from scratch and see if there's some convenient estree builder module I could use. |
https://github.com/davidbonnet/astring could be interesting |
Using https://github.com/babel/babel/blob/master/packages/babel-generator/README.md seems like a good idea to go at it. I also considered using a more appropriate language to do json -> language X but for example ocaml doesn't seem to have an estree generator, only an spidermonkey ast one, so I'm not sure if it's really worth it. There's the possibility of doing it in elixir but I don't know if elixir is that adapted to do that kind of things in a general way. |
After some investigation, this is what babel-register use to compile at runtime, https://github.com/babel/babel/blob/4c371132ae7321f6d08567eab54a59049e07f246/packages/babel-register/src/node.js#L102 The m._compile seems to be doing the code -> runnable thing transformation. I believe it's part of node. Going to investigate this a bit more. Maybe there's something better than eval. Ok so it seems all require does is call vm.runInThisContext , nothing much better than eval, https://github.com/nodejs/node/blob/b488b19eaf2b2e7a3ca5eccd2445e245847a5f76/lib/module.js#L500 https://nodejs.org/api/vm.html#vm_vm_runinthiscontext_code_options Ok so my conclusion from these investigations is there is really nothing wrong in eval'ing code since that's what require do. |
Next step : looking into babel helpers and their visitor pattern and see if they are useful only for transforming estree to estree or if they could be useful here. Babel-template can be interesting maybe : it's used in some of babel code to turn an es6 string into estree. Example : https://github.com/babel/babel/blob/master/packages/babel-helpers/src/helpers.js |
Simple example of babel-generator usage: const generate=require('babel-generator');
const t=require('babel-types');
var ifStatement = t.ifStatement(
t.stringLiteral("top cond"),
t.whileStatement(
t.stringLiteral("while cond"),
t.ifStatement(
t.stringLiteral("nested"),
t.expressionStatement(t.numericLiteral(1))
)
),
t.expressionStatement(t.stringLiteral("alt"))
);
console.log(generate.default(ifStatement).code); |
…marks, tests. Pass test + 10x read perf improvement
Got a 10x perf improvement in reading.
|
To put the code in one function, the variables need unique names. try to follow something like https://gist.github.com/hansihe/b5b1754e2447c50680fc |
idea to get things more concrete :
|
I may work on this again someday, for now closing |
eval("produceArgs(typeArgs)")
brings a 1.5x improvement