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

fs: added _writev (up to x50 faster writes) #2167

Closed
wants to merge 1 commit into from

Conversation

ronkorving
Copy link
Contributor

fs.WriteStream did not yet have a _writev method, so writing many buffers at once could not be optimized by libuv. This adds the function.

I've not yet benchmarked the difference before/after, as I'm not sure how to effectively benchmark disk I/O to be honest (because of disk cache, SSD vs. spinning disk (I'm using a mac fusion drive, that doesn't help)).
Update: benchmarks below

However, with this change:

  • There will be less system calls
  • There will be less jumping between JS land and native land

So I expect throughput to increase a little bit, while at the same time reducing CPU usage.

I didn't add tests, as this was already covered by existing tests (which failed during implementation, but now pass).

Warning: this is my first PR to io.js, so please be rigorous in review.

@brendanashworth brendanashworth added c++ Issues and PRs that require attention from people who are familiar with C++. fs Issues and PRs related to the fs subsystem / file system. labels Jul 12, 2015
@reqshark
Copy link

we should see more efficient data transfer from any program that avoids redundant data copies between intermediate buffers and reduces the number of context switches between user space and kernel space

scatter/gather I/O! 👍

@ronkorving
Copy link
Contributor Author

I noticed that with a very large amount of buffers, we hit a limit and writev (in libuv) fails with EINVAL (cc @bnoordhuis). This can be seen when running the fs benchmark.

Ideally, libuv would detect uv__fs_write getting too many buffers (through uv__getiovmax) and split up the writes into multiple writev calls.

@ronkorving
Copy link
Contributor Author

After patching libuv (I'll send a PR) I got the problem to disappear.
Update: there already is a PR to address the issue: libuv/libuv#269
Update 2: the work has moved into PR libuv/libuv#448

Benchmark on OSX by running ./iojs benchmark/common.js fs write-stream-throughput

Before (master):

fs/write-stream-throughput.js dur=5 type=buf size=2: 0.09258
fs/write-stream-throughput.js dur=5 type=buf size=1024: 43.09845
fs/write-stream-throughput.js dur=5 type=buf size=65535: 268.53789
fs/write-stream-throughput.js dur=5 type=buf size=1048576: 253.07612
fs/write-stream-throughput.js dur=5 type=asc size=2: 0.09160
fs/write-stream-throughput.js dur=5 type=asc size=1024: 42.27883
fs/write-stream-throughput.js dur=5 type=asc size=65535: 253.94307
fs/write-stream-throughput.js dur=5 type=asc size=1048576: 272.43421
fs/write-stream-throughput.js dur=5 type=utf size=2: 0.09103
fs/write-stream-throughput.js dur=5 type=utf size=1024: 40.72314
fs/write-stream-throughput.js dur=5 type=utf size=65535: 290.43753
fs/write-stream-throughput.js dur=5 type=utf size=1048576: 254.93646

After:

fs/write-stream-throughput.js dur=5 type=buf size=2: 8.09139
fs/write-stream-throughput.js dur=5 type=buf size=1024: 239.52816
fs/write-stream-throughput.js dur=5 type=buf size=65535: 271.13331
fs/write-stream-throughput.js dur=5 type=buf size=1048576: 272.94912
fs/write-stream-throughput.js dur=5 type=asc size=2: 2.95558
fs/write-stream-throughput.js dur=5 type=asc size=1024: 210.82502
fs/write-stream-throughput.js dur=5 type=asc size=65535: 234.68711
fs/write-stream-throughput.js dur=5 type=asc size=1048576: 294.56500
fs/write-stream-throughput.js dur=5 type=utf size=2: 2.74160
fs/write-stream-throughput.js dur=5 type=utf size=1024: 184.46578
fs/write-stream-throughput.js dur=5 type=utf size=65535: 284.54823
fs/write-stream-throughput.js dur=5 type=utf size=1048576: 279.93069

The impact is (not unexpectedly) much bigger with many buffers (ie: the case where buffer size is small). I must note though that being on a hybrid SSD/HD (fusion drive) makes this not the most scientifically sound benchmark.

@ronkorving ronkorving changed the title fs: added _writev for faster writes in the case of many buffers fs: added _writev (up to x100 faster writes) Jul 13, 2015
static void WriteBuffers(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);

if (!args[0]->IsInt32())
Copy link
Contributor

Choose a reason for hiding this comment

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

Because this binding isn't accessible to the end user, we typically leave out the well-worded error messages and instead leave in CHECK()s, like this:

CHECK(args[0]->IsInt32());
CHECK(args[1]->IsArray());

Here is an example.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Right, I kinda went with what I saw in WriteBuffer, which is located right above my WriteBuffers function. It too has a test like:

  if (!args[0]->IsInt32())
    return env->ThrowTypeError("First argument must be file descriptor");

I just followed that.

@brendanashworth
Copy link
Contributor

Thanks for taking the time to put this together! I left a few small nits. The benchmark does seem to be looking good right now, but we do have to wait for that libuv patch. Hopefully that will work out well and make its way into the code base.

@Fishrock123 Fishrock123 added the libuv Issues and PRs related to the libuv dependency or the uv binding. label Jul 16, 2015
@ronkorving
Copy link
Contributor Author

No worries! I've fixed those few little things (thanks for the review!), so now all attention goes back to libuv. If the original author of that PR does not respond before Monday or so, I will dive in and fix it myself.

var req = new FSReqWrap();
req.oncomplete = wrapper;
binding.writeBuffers(fd, chunks, position, req);
};
Copy link
Contributor

Choose a reason for hiding this comment

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

; is not necessary for function declarations

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Nice catch, I wonder why the linter didn't catch that.

Copy link
Member

Choose a reason for hiding this comment

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

@ronkorving it is because the related eslint rule (no-extra-semi) is not enabled.
I have a working branch to fix that. @silverwind what do you think about it ?

Copy link
Contributor

Choose a reason for hiding this comment

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

I've been meaning to look at enabling semicolon related rules, but didn't find time yet. Go ahead and do a PR if you like. One commit to enable the rule(s), another to fix any issues.

Copy link
Contributor

Choose a reason for hiding this comment

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

@ronkorving Can you remove this ;?

const char* buf = Buffer::Data(chunk);
size_t len = Buffer::Length(chunk);

iovs[i] = uv_buf_init(const_cast<char *>(buf), len);
Copy link
Contributor

Choose a reason for hiding this comment

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

style nit: <char*>

Copy link
Contributor

Choose a reason for hiding this comment

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

note: I see it's done that way in Read(), but that style originated with Ryan way back in 2010. The style has changed and we just propagate w/ new code.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Understood.

@trevnorris
Copy link
Contributor

Will need docs and tests. Also I would recommend exposing this via fs.writev. Or hooking into fs.write to detect if an array is passed. This would be very helpful to access directly.

Some comments and style nits listed. Looking good.

@ronkorving
Copy link
Contributor Author

I agree it would be interesting to expose this through fs.write or fs.writev, but do you think that should be part of this PR? I don't mind iterating in multiple smaller PRs, especially since I expect some debate over API on that one, and I see little reason for that feature to block this PR. Just my thoughts, but what do you think @trevnorris ?

@ronkorving
Copy link
Contributor Author

I quote:

fs: implemented WriteStream#writev
Streams with writev allow many buffers to be pushed to underlying
OS APIs in one batch, in this case improving write throughput by
an order of magnitude. This is especially noticable when writing
many (small) buffers.

@trevnorris
Copy link
Contributor

@ronkorving WTF. Mobile output wasn't showing the extended comment. Thanks. Reads well.

trevnorris pushed a commit that referenced this pull request Sep 14, 2015
Streams with writev allow many buffers to be pushed to underlying OS
APIs in one batch, in this case improving write throughput by an order
of magnitude. This is especially noticeable when writing many (small)
buffers.

PR-URL: #2167
Reviewed-By: Trevor Norris <trev.norris@gmail.com>
@trevnorris
Copy link
Contributor

Thanks. Landed on 05d30d5 and removed extra semicolon.

@trevnorris trevnorris closed this Sep 14, 2015
Fishrock123 pushed a commit that referenced this pull request Sep 15, 2015
Streams with writev allow many buffers to be pushed to underlying OS
APIs in one batch, in this case improving write throughput by an order
of magnitude. This is especially noticeable when writing many (small)
buffers.

PR-URL: #2167
Reviewed-By: Trevor Norris <trev.norris@gmail.com>
@rvagg rvagg added the semver-minor PRs that contain new features and should be released in the next minor version. label Sep 15, 2015
@rvagg
Copy link
Member

rvagg commented Sep 15, 2015

This is semver-minor right? tagging it as so, @trevnorris @ronkorving please correct me if I'm wrong

@rvagg
Copy link
Member

rvagg commented Sep 15, 2015

@Fishrock123 if you've cherry-picked this then the first bump of 4.x will have to be 4.1.0

@ronkorving
Copy link
Contributor Author

I'm fine either way, but how is this not patch? It introduces internal-only API for improved performance that is not exposed in any way to the outside world. Is patch only allowed to be bugfixes?

@rvagg
Copy link
Member

rvagg commented Sep 15, 2015

Hmm, perhaps you're right, I saw env->SetMethod(target, "writeBuffers", WriteBuffers); and jumped to the conclusion. This is only exposed via process.binding('fs').writeBuffers right? Are we happy to say that everything in process.binding is OOB for semver- labels and it doesn't matter what we do in there that we don't have to version for it? Thoughts anyone else?

@rvagg rvagg mentioned this pull request Sep 15, 2015
@Fishrock123
Copy link
Contributor

cc @nodejs/tsc, is this minor or patch?

I'm currently leaning toward minor. If our first post 4.0.0 is a minor it doesn't bother me. Such is the way of SemVer.

@trevnorris
Copy link
Contributor

Are we happy to say that everything in process.binding is OOB for semver

I really hope so, or else we've all been breaking the rules. :P

@rvagg
Copy link
Member

rvagg commented Sep 16, 2015

Here's my take on it: this PR adds two publicly accessible things, process.binding('fs').writeBuffers and fs.WriteStream.prototype._writev. Regardless of the fact that it's on process.bindings or in an underscore method, people will use it, perhaps even readable-stream could benefit from using it directly in some way (that's not likely but consider it an example).

Our approach to semver shouldn't be to try and pull things down to the minimum but just the opposite. We should be using the maximum part of semver that could possibly make sense for a change. If something might break someone's workflow then it should be semver-major, if something we add might be used by end-users then it should be a semver-minor. semver-patch should only be used reserved for cases where we have a high degree of certainty that it's not going to break for someone and that it's not adding new surface area to our exposed API.

So I'm still +1 on this being semver-minor even if there's no documentation added for what's been added.

Also, thanks @ronkorving, this a great addition!

@ronkorving
Copy link
Contributor Author

You're welcome :) I'm just happy to see it land, no matter the version (I'll leave that up to your judgement).

Fishrock123 added a commit to Fishrock123/node that referenced this pull request Sep 17, 2015
Notable changes:

* buffer:
  - Buffers are now created in JavaScript, rather than C++. This increases the speed of buffer creation (Trevor Norris) nodejs#2866.
  - `Buffer#slice()` now uses `Uint8Array#subarray()` internally, increasing `slice()` performance (Karl Skomski) nodejs#2777.
* fs:
  - `fs.utimes()` now properly converts numeric strings, `NaN`, and `Infinity` (Yazhong Liu) nodejs#2387.
  - `fs.WriteStream` now implements `_writev`, allowing for super-fast bulk writes (Ron Korving) nodejs#2167.
* http: Fixed an issue with certain `write()` sizes causing errors when using `http.request()` (Fedor Indutny) nodejs#2824.
* npm: Upgrade to version 2.14.3, see https://github.com/npm/npm/releases/tag/v2.14.3 for more details (Kat Marchán) nodejs#2822.
* src: V8 cpu profiling no longer erroneously shows idle time (Oleksandr Chekhovskyi) nodejs#2324.
* v8: Lateral upgrade to 4.5.103.33 from 4.5.103.30, contains minor fixes (Ali Ijaz Sheikh) nodejs#2870.
  - This fixes a previously known bug where some computed object shorthand properties did not work correctly (nodejs#2507).

Refs: nodejs#2844
PR-URL: nodejs#2889
Fishrock123 added a commit that referenced this pull request Sep 17, 2015
Notable changes:

* buffer:
  - Buffers are now created in JavaScript, rather than C++. This increases the speed of buffer creation (Trevor Norris) #2866.
  - `Buffer#slice()` now uses `Uint8Array#subarray()` internally, increasing `slice()` performance (Karl Skomski) #2777.
* fs:
  - `fs.utimes()` now properly converts numeric strings, `NaN`, and `Infinity` (Yazhong Liu) #2387.
  - `fs.WriteStream` now implements `_writev`, allowing for super-fast bulk writes (Ron Korving) #2167.
* http: Fixed an issue with certain `write()` sizes causing errors when using `http.request()` (Fedor Indutny) #2824.
* npm: Upgrade to version 2.14.3, see https://github.com/npm/npm/releases/tag/v2.14.3 for more details (Kat Marchán) #2822.
* src: V8 cpu profiling no longer erroneously shows idle time (Oleksandr Chekhovskyi) #2324.
* v8: Lateral upgrade to 4.5.103.33 from 4.5.103.30, contains minor fixes (Ali Ijaz Sheikh) #2870.
  - This fixes a previously known bug where some computed object shorthand properties did not work correctly (#2507).

Refs: #2844
PR-URL: #2889
@ronkorving ronkorving deleted the fs-writev branch September 17, 2015 05:51
@rvagg rvagg mentioned this pull request Sep 22, 2015
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
c++ Issues and PRs that require attention from people who are familiar with C++. fs Issues and PRs related to the fs subsystem / file system. semver-minor PRs that contain new features and should be released in the next minor version.
Projects
None yet
Development

Successfully merging this pull request may close these issues.