Skip to content

Commit 450c3d9

Browse files
committed
[GR-17457] Avoid some additional copy steps while reading from io.
PullRequest: truffleruby/3079
2 parents bc5e710 + f23c205 commit 450c3d9

File tree

6 files changed

+136
-14
lines changed

6 files changed

+136
-14
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ Performance:
6060
* Reduce conversion of `VALUE`s to native handle during common operations in C extensions (@aardvark179).
6161
* Improved performance of regex boolean matches (e.g., `Regexp#match?`) by avoiding match data allocation in TRegex (#2558, @nirvdrum).
6262
* Remove overhead when getting using `RDATA_PTR` (@aardvark179).
63+
* Additional copy operations have been reduced when performing IO (#2536, @aardvark179).
6364

6465
Changes:
6566

src/main/java/org/truffleruby/core/thread/ThreadLocalBuffer.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
public final class ThreadLocalBuffer {
1818

1919
public static final ThreadLocalBuffer NULL_BUFFER = new ThreadLocalBuffer(new Pointer(0, 0), null);
20+
private static final long ALIGNMENT = 8L;
21+
private static final long ALIGNMENT_MASK = ALIGNMENT - 1;
2022

2123
public final Pointer start;
2224
long remaining;
@@ -73,7 +75,7 @@ public Pointer allocate(RubyThread thread, long size, ConditionProfile allocatio
7375

7476
/* We ensure we allocate a non-zero number of bytes so we can track the allocation. This avoids returning null
7577
* or reallocating a buffer that we technically have a pointer to. */
76-
final long allocationSize = Math.max(size, 4);
78+
final long allocationSize = alignUp(size);
7779
if (allocationProfile.profile(remaining >= allocationSize)) {
7880
final Pointer pointer = new Pointer(cursor(), allocationSize);
7981
remaining -= allocationSize;
@@ -88,6 +90,10 @@ public Pointer allocate(RubyThread thread, long size, ConditionProfile allocatio
8890
}
8991
}
9092

93+
private static long alignUp(long size) {
94+
return (size + ALIGNMENT_MASK) & ~ALIGNMENT_MASK;
95+
}
96+
9197
@TruffleBoundary
9298
private ThreadLocalBuffer allocateNewBlock(RubyThread thread, long size) {
9399
// Allocate a new buffer. Chain it if we aren't the default thread buffer, otherwise make a new default buffer.

src/main/java/org/truffleruby/extra/ffi/Pointer.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,24 @@ public void readBytes(long offset, byte[] buffer, int bufferPos, int length) {
185185
UNSAFE.copyMemory(null, address + offset, buffer, Unsafe.ARRAY_BYTE_BASE_OFFSET + bufferPos, length);
186186
}
187187

188+
@TruffleBoundary
189+
public boolean readBytesCheck8Bit(byte[] buffer, int length) {
190+
assert address != 0 || length == 0;
191+
assert buffer != null;
192+
assert length >= 0;
193+
194+
long base = address;
195+
boolean highBitUsed = false;
196+
for (int i = 0; i < length; i++) {
197+
byte aByte = UNSAFE.getByte(null, base + i);
198+
if (aByte < 0) {
199+
highBitUsed = true;
200+
}
201+
buffer[i] = aByte;
202+
}
203+
return highBitUsed;
204+
}
205+
188206
public short readShort(long offset) {
189207
assert address + offset != 0;
190208
return UNSAFE.getShort(address + offset);

src/main/java/org/truffleruby/extra/ffi/PointerNodes.java

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import org.truffleruby.core.rope.RopeConstants;
3232
import org.truffleruby.core.rope.RopeNodes;
3333
import org.truffleruby.core.string.RubyString;
34+
import org.truffleruby.core.support.RubyByteArray;
3435
import org.truffleruby.core.numeric.RubyBignum;
3536
import org.truffleruby.core.symbol.RubySymbol;
3637
import org.truffleruby.language.Nil;
@@ -316,6 +317,27 @@ protected RubyString readStringToNull(long address, Nil limit,
316317

317318
}
318319

320+
@Primitive(name = "pointer_read_bytes_to_byte_array", lowerFixnum = { 1, 3 })
321+
public abstract static class PointerReadBytesToArrayNode extends PointerPrimitiveArrayArgumentsNode {
322+
323+
@Specialization
324+
protected Object readBytes(RubyByteArray array, int arrayOffset, long address, int length,
325+
@Cached ConditionProfile zeroProfile,
326+
@Cached RopeNodes.MakeLeafRopeNode makeLeafRopeNode) {
327+
final Pointer ptr = new Pointer(address);
328+
if (zeroProfile.profile(length == 0)) {
329+
// No need to check the pointer address if we read nothing
330+
return nil;
331+
} else {
332+
checkNull(ptr);
333+
final byte[] bytes = array.bytes;
334+
ptr.readBytes(0, bytes, arrayOffset, length);
335+
return nil;
336+
}
337+
}
338+
339+
}
340+
319341
@Primitive(name = "pointer_read_bytes", lowerFixnum = 1)
320342
public abstract static class PointerReadBytesNode extends PointerPrimitiveArrayArgumentsNode {
321343

@@ -337,9 +359,10 @@ protected RubyString readBytes(long address, int length,
337359
} else {
338360
checkNull(ptr);
339361
final byte[] bytes = new byte[length];
340-
ptr.readBytes(0, bytes, 0, length);
362+
final boolean is8Bit = ptr.readBytesCheck8Bit(bytes, length);
341363
final Rope rope = makeLeafRopeNode
342-
.executeMake(bytes, ASCIIEncoding.INSTANCE, CodeRange.CR_UNKNOWN, NotProvided.INSTANCE);
364+
.executeMake(bytes, ASCIIEncoding.INSTANCE, is8Bit ? CodeRange.CR_VALID : CodeRange.CR_7BIT,
365+
length);
343366
final RubyString instance = new RubyString(
344367
coreLibrary().stringClass,
345368
getLanguage().stringShape,

src/main/ruby/truffleruby/core/io.rb

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -141,18 +141,18 @@ def fill(io, max = DEFAULT_READ_SIZE)
141141

142142
count = Primitive.min(unused, max)
143143

144-
buffer = Truffle::POSIX.read_string_at_least_one_byte(io, count)
145-
bytes_read = buffer ? buffer.bytesize : 0
146-
if bytes_read > 0
144+
total_read = 0
145+
Truffle::POSIX.read_to_buffer_at_least_one_byte(io, count) do |pointer, bytes_read|
147146
# Detect if another thread has updated the buffer
148147
# and now there isn't enough room for this data.
149148
if bytes_read > unused
150149
raise RuntimeError, 'internal implementation error - IO buffer overrun'
151150
end
152-
@storage.fill(@used, buffer, 0, bytes_read)
151+
Primitive.pointer_read_bytes_to_byte_array(@storage, @used, pointer.address, bytes_read)
153152
@used += bytes_read
153+
total_read += bytes_read
154154
end
155-
bytes_read
155+
total_read
156156
end
157157

158158
# A request to the buffer to have data. The buffer decides whether
@@ -1265,9 +1265,13 @@ def read_to_separator_with_limit
12651265
# Method G
12661266
def read_all
12671267
str = +''
1268-
until @buffer.exhausted?
1269-
@buffer.fill_from @io
1270-
str << @buffer.shift
1268+
unless @buffer.exhausted?
1269+
tmp_str = @buffer.shift
1270+
str << tmp_str unless tmp_str.empty?
1271+
end
1272+
1273+
while (tmp_str = Truffle::POSIX.read_string_at_least_one_byte(@io, InternalBuffer::DEFAULT_READ_SIZE))
1274+
str << tmp_str
12711275
end
12721276

12731277
yield_string(str) { |s| yield s }
@@ -1787,9 +1791,14 @@ def read(length=nil, buffer=nil)
17871791
# If the buffer is already exhausted, returns +""+.
17881792
private def read_all
17891793
str = +''
1790-
until @ibuffer.exhausted?
1791-
@ibuffer.fill_from self
1792-
str << @ibuffer.shift
1794+
unless @ibuffer.exhausted?
1795+
if !(tmp_str = @ibuffer.shift).empty?
1796+
str << tmp_str
1797+
end
1798+
end
1799+
1800+
while (tmp_str = Truffle::POSIX.read_string_at_least_one_byte(self, InternalBuffer::DEFAULT_READ_SIZE))
1801+
str << tmp_str
17931802
end
17941803

17951804
str

src/main/ruby/truffleruby/core/posix.rb

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,21 @@ def self.read_string_at_least_one_byte(io, count)
362362
end
363363
end
364364

365+
# Read up to count bytes of io to the thread-local IO buffer, and
366+
# yields the buffer (a FFI::Pointer) and bytes_read
367+
def self.read_to_buffer_at_least_one_byte(io, count, &block)
368+
while true
369+
# must call #read_to_buffer in order to properly support polyglot STDIO
370+
bytes_read, errno = read_to_buffer(io, count, &block)
371+
return bytes_read if errno == 0
372+
if errno == EAGAIN_ERRNO
373+
IO.select([io])
374+
else
375+
Errno.handle_errno(errno)
376+
end
377+
end
378+
end
379+
365380
# Used in IO#read_nonblock
366381

367382
def self.read_string_nonblock(io, count, exception)
@@ -405,6 +420,54 @@ def self.read_string_native(io, length)
405420
end
406421
end
407422

423+
def self.read_to_buffer_native(io, length)
424+
fd = io.fileno
425+
buffer = Primitive.io_thread_buffer_allocate(length)
426+
begin
427+
bytes_read = Truffle::POSIX.read(fd, buffer, length)
428+
if bytes_read < 0
429+
bytes_read, errno = bytes_read, Errno.errno
430+
elsif bytes_read == 0 # EOF
431+
bytes_read, errno = 0, 0
432+
else
433+
bytes_read, errno = bytes_read, 0
434+
end
435+
436+
if bytes_read < 0
437+
[-1, errno]
438+
elsif bytes_read == 0 # EOF
439+
[0, 0]
440+
else
441+
yield buffer, bytes_read
442+
[bytes_read, 0]
443+
end
444+
ensure
445+
Primitive.io_thread_buffer_free(buffer)
446+
end
447+
end
448+
449+
def self.read_to_buffer_polyglot(io, length, &block)
450+
fd = io.fileno
451+
if fd == 0
452+
buffer = Primitive.io_thread_buffer_allocate(length)
453+
begin
454+
read = Primitive.io_read_polyglot length
455+
if read
456+
bytes_read = read.bytesize
457+
buffer.write_string_length(read, bytes_read)
458+
yield buffer, bytes_read
459+
[bytes_read, 0]
460+
else
461+
[0, 0]
462+
end
463+
ensure
464+
Primitive.io_thread_buffer_free(buffer)
465+
end
466+
else
467+
read_to_buffer_native(io, length, &block)
468+
end
469+
end
470+
408471
def self.read_string_polyglot(io, length)
409472
fd = io.fileno
410473
if fd == 0
@@ -513,12 +576,14 @@ def self.write_string_nonblock_polyglot(io, string)
513576
if Truffle::Boot.get_option('polyglot-stdio')
514577
class << self
515578
alias_method :read_string, :read_string_polyglot
579+
alias_method :read_to_buffer, :read_to_buffer_polyglot
516580
alias_method :write_string, :write_string_polyglot
517581
alias_method :write_string_nonblock, :write_string_nonblock_polyglot
518582
end
519583
else
520584
class << self
521585
alias_method :read_string, :read_string_native
586+
alias_method :read_to_buffer, :read_to_buffer_native
522587
alias_method :write_string, :write_string_native
523588
alias_method :write_string_nonblock, :write_string_nonblock_native
524589
end

0 commit comments

Comments
 (0)