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

optimize passing arguments from js to c #327

Closed
jorangreef opened this issue Jul 11, 2018 · 13 comments
Closed

optimize passing arguments from js to c #327

jorangreef opened this issue Jul 11, 2018 · 13 comments

Comments

@jorangreef
Copy link

See #55 (comment) for context.

@jorangreef
Copy link
Author

@jorangreef I tested the uint32_t truncation, and AFAICT it's working as advertised. Please have a look at the PR and let me know if I've missed any scenarios!

Thanks @gabrielschulhof, I see that you asserted that overflow happens when calling napi_get_value_uint32() on a JS value that is more than 32 bits: testUint32(4294967297, 1);

What I meant was that if the JS value is more than 32 bits, I would have expected an exception to be thrown instead. However, this kind of check should rather be in napi_get_value_uint32_validated() (or something like that) to avoid slowing down napi_get_value_uint32().

I just realized that using a large ArrayBuffer to hold the arguments implies that the memory for the two Buffers is contiguous, otherwise it requires copying. Is this the case for your addon and, if not, can you rearrange the memory to make this the case?

Yes, I thought of this but the addon needs to be zero-copy, and the key and value buffer arguments would not necessarily always be the same.

@jorangreef I'm curious - are you using N-API directly, or via the node-addon-api package?

I am using N-API directly, using #include <node_api.h> running Node v10.6.0.

Thanks for all your help with this!

@jorangreef
Copy link
Author

I know we always compare N-API to v8, but is there any chance that some of the v8 methods we're comparing against are slower than they should be?

@gabrielschulhof
Copy link
Collaborator

@jorangreef when I build the comparison binding in V8 I try to use the most efficient method for grabbing the arguments from the engine.

@gabrielschulhof
Copy link
Collaborator

@jorangreef have you tried switching from node::Buffer to ArrayBuffer? AFAICT, node::Buffer uses an ArrayBufferView which seems to be an additional level of indirection.

@gabrielschulhof
Copy link
Collaborator

I tested some more combinations, and it looks like if you switch to ArrayBuffer and we provide validation-free versions of napi_get_arraybuffer_info() and napi_get_value_int64() then we can nearly erase the impact of switching from V8 to N-API.

More Buffer/ArrayBuffer/Int64 Combinations

@nodejs/addon-api this supports that we should provide validation-free versions of the convert-to-native-type APIs.

@gabrielschulhof
Copy link
Collaborator

@mhdawson
Copy link
Member

Sorry I could look at the code, but for clarity in this issue, what do you mean by "validation-free"

@gabrielschulhof
Copy link
Collaborator

@mhdawson by "validation-free" I mean a version of the API where we don't call APIs such as ->IsNumber() after casting the napi_value as a v8::Local<v8::Value>. We simply assume that it's a number and retrieve the value.

Similarly, we do not validate that the napi_value is a v8::ArrayBuffer, but we simply .As<v8::ArrayBuffer>() and then proceed to retrieve the pointer to the data and the length.

We leave the validation (if any) to the addon author and document that the API doesn't perform any validation, all in the name of better performance.

@gabrielschulhof
Copy link
Collaborator

gabrielschulhof commented Jul 19, 2018

@mhdawson I ran individual benchmarks for each data type to test whether removing the validation would improve the performance to some significance, but the only data type for which this was the case was Int64. All the convert-to-native APIs have pretty much the same performance whether or not we validate.

I suspect that's because in napi_get_value_int64() we not only validate that the value we are asked to convert to native is indeed a JavaScript number, but we also perform some sanitization of the value:

  if (val->IsInt32()) {
    *result = val.As<v8::Int32>()->Value();
    return napi_clear_last_error(env);
  }

  RETURN_STATUS_IF_FALSE(env, val->IsNumber(), napi_number_expected);

  // v8::Value::IntegerValue() converts NaN, +Inf, and -Inf to INT64_MIN,
  // inconsistent with v8::Value::Int32Value() which converts those values to 0.
  // Special-case all non-finite values to match that behavior.
  double doubleValue = val.As<v8::Number>()->Value();
  if (std::isfinite(doubleValue)) {
    // Empty context: https://github.com/nodejs/node/issues/14379
    v8::Local<v8::Context> context;
    *result = val->IntegerValue(context).FromJust();
  } else {
    *result = 0;
}

the performance gain stems from reducing all this to just

    *result = val->IntegerValue(context).FromJust();

foregoing the special-casing mentioned in the comment.

So, a new, non-validating API really only makes sense for Int64:

napi-fast-string

napi-fast-double

napi-fast-int64

@jorangreef
Copy link
Author

I tested some more combinations, and it looks like if you switch to ArrayBuffer and we provide validation-free versions of napi_get_arraybuffer_info() and napi_get_value_int64() then we can nearly erase the impact of switching from V8 to N-API.

Thanks @gabrielschulhof for the tremendous effort.

@mhdawson
Copy link
Member

@gabrielschulhof thanks for all of the analysis. One last question,

Did you check the perf if this part is left in

  if (val->IsInt32()) {
    *result = val.As<v8::Int32>()->Value();
    return napi_clear_last_error(env);
  }

@gabrielschulhof
Copy link
Collaborator

@mhdawson we can leave it in. It has no perf impact.

gabrielschulhof pushed a commit to gabrielschulhof/node that referenced this issue Sep 6, 2018
Adds `napi_get_value_int64_unsafe()` which skips the correction
whereby non-finite values are rendered as zero, exposing instead
the engine-specific behavior.

This tradeoff favors performance-intensive applications where
guarantees of finiteness can be made a priori.

This modification is further motivated by the fact that there is
currently no typed array support for `int64_t`.

Fixes: nodejs/abi-stable-node#327
@mhdawson
Copy link
Member

Looks like the related PR has landed. Closing.

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

Successfully merging a pull request may close this issue.

3 participants