Open wooster0 opened 5 years ago
can we just special case String#[]
with the optimization if index == 0
?
I don't know, it looks like the difference boils down to the added bounds checking and support for negative indexes in byte_at
, otherwise the codepaths look pretty identical (char_at
is already optimized for the ascii only case).
require "benchmark"
class String
def first_char
c = each_char do |char|
break char
end
raise IndexError.new unless c
c
end
def at2(index)
if index == 0
first_char
else
at(index)
end
end
end
Benchmark.ips do |bm|
bm.report "first_char" do
"hello".first_char
end
bm.report "[0]" do
"hello"[0]
end
bm.report "at2" do
"hello".at2(0)
end
end
first_char 552.12M ( 1.81ns) (± 2.14%) 0.0B/op fastest
[0] 386.78M ( 2.59ns) (± 1.67%) 0.0B/op 1.43× slower
at2 547.62M ( 1.83ns) (± 3.85%) 0.0B/op 1.01× slower
Try with a string that's not hardcoded (for example reading from ARGV). The difference is miniscule:
require "benchmark"
str = ARGV[0]
Benchmark.ips do |bm|
bm.report "each_char" do
str.each_char do |char|
break char
end
end
bm.report "[0]" do
str[0]
end
end
each_char 292.54M ( 3.42ns) (± 3.93%) 0.0B/op fastest
[0] 273.98M ( 3.65ns) (± 6.93%) 0.0B/op 1.07× slower
And if you change the order you get that the second one is fastest.
Can we stop worrying about sub-nanosecond performance differences?
Thank you!
All these examples are not really worth anything as @asterite shows. That's because #char_at
is super efficient for ascii-only strings because it only reads the byte at index n and converts it to Char
(=> N(1)
).
But with strings containing non-ascii characters, there could be a significant difference and optimizing for 0
might actually be worth it. Maybe even for other small indices, where each_char
+ break
would be faster than the current char_at
implementation.
Feel free to continue this.
IMO there could be a method that converts a String
to a Char
if and only if it consists of exactly one character, not because of performance, but because it coincides with what sprintf "%c"
could theoretically allow: (see #11022)
class String
def chr
# TODO: do this without computing `@length` or using `Char::Reader`
size == 1 ? self[0] : raise ArgumentError.new
end
end
struct Char
def chr
self
end
end
Then either #chr
or another name like #to_char
could be used to check which arguments are supported by %c
.
Note that on a fresh String
whose @length == 0
, such as from String.build
, the fastest way is to use Char::Reader
regardless of its codepoint range:
require "benchmark"
def build
String.build do |io|
100.times { io << "abcde" }
end
end
str = ""
ch = 'a'
Benchmark.ips do |bm|
bm.report "control" do
str = build
end
bm.report "each_char" do
str = build
ch = str.each_char do |char|
break char
end || raise IndexError.new
end
bm.report "[0]" do
str = build
ch = str[0]
end
bm.report "Char::Reader" do
str = build
ch = str.empty? ? raise IndexError.new : Char::Reader.new(str).current_char
end
end
control 1.69M (591.62ns) (± 3.23%) 3.02kB/op 1.00× slower
each_char 1.20M (836.77ns) (± 2.96%) 3.02kB/op 1.41× slower
[0] 1.20M (836.18ns) (± 2.79%) 3.02kB/op 1.41× slower
Char::Reader 1.69M (591.36ns) (± 3.96%) 3.02kB/op fastest
This is because #each_char
and #char_at
both call #single_byte_optimizable?
first, which calls #size
, which iterates through the entire string as @length == 0
. In contrast, a Char::Reader
can operate without the need for String#size
at all. One thing we could avoid here is the double iteration between #single_byte_optimizable?
and #char_index_to_byte_index
: we could cache the real @length
and compute the result of #char_at
in a single Char::Reader
pass. It probably won't look beautiful though.
Related: #12107
I suppose we should combine #single_byte_optimizable?
with #size_known?
. If the size is unknown, it's not optimizable.
Presumably, a method that fetches a Char
at a given byte index (#char_at_byte(Int32) : Char
) could be a useful addition and would work efficiently for this use case.
I think a method should be added to
String
that returns the first character and should be a faster alternative to[0]
. The strategy is to start an iteration through the string but stop at the first character. That's faster than[0]
. Reason for this is for example theeach_byte
on ASCII-only optimization inString#each_char
.The first is also faster for non-ASCII characters. This optimization is for example already used here:
string.cr:4069
. The method could perhaps be calledfirst
orfirst_char
.