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

Add Unix.nonblock_single_write and Unix.nonblock_read #13024

Open
wants to merge 26 commits into
base: trunk
Choose a base branch
from

Conversation

craff
Copy link
Contributor

@craff craff commented Mar 11, 2024

This function are around 30% faster when avoiding the copying which is necessary if one release the global lock.
They are very useful for fast modern server doing non blocking IO. This addresses issue #11992. Note I could not test the windows code!

@craff
Copy link
Contributor Author

craff commented Mar 12, 2024

I did more serious benchmarks (see code) and the gain is between 10 and 20%, larger with larger buffer, arround 5% with 1 byte buffer, but very noisy measure! My laptop is not very stable for benchmark. I would be happy if someone could test on a stable computer for benchmark

Here is the test I use (nonblocking IO, without select):
bench.ml.gz

Compile with:
/usr/local/bin/ocamlopt -I +unix unix.cmxa bench.ml

Run with (second integer is buffer size, first is number of buffer written):
./a.out foo.txt 100000 65536

@OlivierNicole
Copy link
Contributor

I obtain similar speedups in the benchmark (between 25 and 30 %). I am reviewing this PR.

@craff
Copy link
Contributor Author

craff commented Mar 14, 2024

I obtain similar speedups in the benchmark (between 25 and 30 %). I am reviewing this PR.

My laptop was not so good, I am happy that you got 25-30%. I was considering write also the code for write/read on big array. Should this be in the same PR ?

@OlivierNicole
Copy link
Contributor

We probably should wait for the opinion from a maintainer about the current design before extending it. Personally, I think the addition is pleasantly self-contained. Sure, the API doesn’t prevent its user from calling the functions on blocking file descriptors, but the documentation makes this danger very explicit. Preventing such blocking calls by storing the blocking/non-blockingness in the file descriptor would require a much larger PR and I’m not sure the gain would be worth the cost.

Copy link
Contributor

@OlivierNicole OlivierNicole left a comment

Choose a reason for hiding this comment

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

The approach seems sane, I just noticed a few issues in the details of the code. We should also test that the functions run as expected on Windows.

Edit.: thanks for the contribution!

otherlibs/unix/read_unix.c Outdated Show resolved Hide resolved
otherlibs/unix/read_win32.c Outdated Show resolved Hide resolved
otherlibs/unix/write_unix.c Outdated Show resolved Hide resolved
otherlibs/unix/write_win32.c Outdated Show resolved Hide resolved
otherlibs/unix/read_win32.c Outdated Show resolved Hide resolved
otherlibs/unix/unix_win32.ml Outdated Show resolved Hide resolved
otherlibs/unix/unix_win32.ml Outdated Show resolved Hide resolved
otherlibs/unix/write_win32.c Outdated Show resolved Hide resolved
otherlibs/unix/unixsupport_win32.c Outdated Show resolved Hide resolved
otherlibs/unix/write_win32.c Outdated Show resolved Hide resolved
@craff
Copy link
Contributor Author

craff commented Mar 25, 2024

We really need to test this code on a windows platform. Just running the benchmark given above + may be a test for some errors.

@OlivierNicole
Copy link
Contributor

I agree, and one way to do it would be to add a small test of these functions in tests/lib-unix/. Maybe a test of a domain performing a socket read/write at the same time as another domain forces a GC.

However I don’t want to encourage to implement without being sure that the PR has sufficient agreement. I would be more at peace if another maintainer validated the current design.

Copy link
Contributor

@nojb nojb left a comment

Choose a reason for hiding this comment

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

I left some technical comments. Going over the discussion in the motivating issue #11992, there was no opposition but also no clear opinion in favour by the other participants (@gasche and @xavierleroy). Technically, the PR seems sound, but the "safe" part of it is a bit unsatisfying (if we use this call with a blocking file descriptor, the whole system will block). @gasche, @xavierleroy: any opinions?

otherlibs/unix/read_unix.c Outdated Show resolved Hide resolved
otherlibs/unix/unix_win32.ml Outdated Show resolved Hide resolved
otherlibs/unix/unix_win32.ml Outdated Show resolved Hide resolved
otherlibs/unix/unix_unix.ml Outdated Show resolved Hide resolved
otherlibs/unix/unix_unix.ml Outdated Show resolved Hide resolved
@@ -321,6 +321,12 @@ void caml_uerror(const char *cmdname, value cmdarg)
caml_unix_error(errno, cmdname, cmdarg);
}

CAMLprim void caml_unix_uerror(value msg, value cmdarg) {
CAMLparam0();
caml_uerror(String_val(msg), cmdarg);
Copy link
Contributor

Choose a reason for hiding this comment

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

Looking at the source of caml_uerror, it looks like this is safe (the GC will not be triggered before String_val(msg) is copied), but it is subtle and fragile (a change in caml_uerror can silently cause a segfault here). Can we find a more robust way of writing this code?

if (Descr_kind_val(fd) == KIND_SOCKET) {
SOCKET s = Socket_val(fd);
ret = recv(s, &Byte(buf, ofs), len, 0);
if (ret == SOCKET_ERROR) ret = -1;
Copy link
Contributor

Choose a reason for hiding this comment

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

The error handling here is missing something: in case of error you need to call caml_win32_maperr(WSAGetLastError()) in order to set errno correctly. See what is done in the usual read call for inspiration.

// The write handle for an anonymous pipe has been closed. We match the
// Unix behavior, and treat this as a zero-read instead of a Unix_error.
ret = 0;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Idem; here one should call caml_win32_maperr(GetLastError()).

@@ -390,6 +390,12 @@ val read_bigarray :
(** Same as {!read}, but read the data into a bigarray.
@since 5.2 *)

val nonblock_read : file_descr -> bytes -> int -> int -> int
(** Same as {!read}, but does not release the global lock nor copy the
bytes read. It is only safe to use with non blocking file descriptor,
Copy link
Contributor

Choose a reason for hiding this comment

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

It is not completely clear what does "safe" means here. What about "It should only be used with ...".

if (Descr_kind_val(fd) == KIND_SOCKET) {
SOCKET s = Socket_val(fd);
ret = send(s, &Byte(buf, ofs), len, 0);
if (ret == SOCKET_ERROR) ret = -1;
Copy link
Contributor

Choose a reason for hiding this comment

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

Error handling in this function is missing the same as in the "read" function.

@dbuenzli
Copy link
Contributor

(if we use this call with a blocking file descriptor, the whole system will block)

Indeed and as such I find the name to be quite misleading. It may feel absurd but I think blocking_{read,write} (or another perhaps another convention to find) would be more enlighting to grep for the day this actually happens.

@craff
Copy link
Contributor Author

craff commented Apr 22, 2024

(if we use this call with a blocking file descriptor, the whole system will block)

Indeed and as such I find the name to be quite misleading. It may feel absurd but I think blocking_{read,write} (or another perhaps another convention to find) would be more enlighting to grep for the day this actually happens.

  • Using blocking is confusing because it is mainly useful with non-blocking socket
  • Using non-blocking is confusing because it will block the whole program if you are using blocking socket.

I really do not know a good name ;-)

@dbuenzli
Copy link
Contributor

Here are a few thoughts:

  1. People tend to tab complete without thinking much. As such between non_blocking and blocking I would rather use blocking. Experts will know what they are doing.
  2. Thinking about ctypes's ?release_runtime_lock:bool argument. Maybe something themed that way, perhaps keep_runtime_lock_and_{read,write}. It's not as if one uses these functions everywhere I don't mind the long name.

@OlivierNicole
Copy link
Contributor

I like keep_runtime_lock_and_{read,write}.

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 this pull request may close these issues.

None yet

5 participants