Skip to content

Commit

Permalink
Micro optimize encoding checks
Browse files Browse the repository at this point in the history
Profiling shows a lot of time spent in various encoding check functions.
I'm working on optimizing them on the Ruby side, but if we assume most
strings are one of the simple 3 encodings, we can skip a lot of overhead.

```ruby
require 'strscan'
require 'benchmark/ips'

source = 10_000.times.map { rand(9999999).to_s }.join(",").force_encoding(Encoding::UTF_8).freeze

def scan_to_i(source)
  scanner = StringScanner.new(source)
  while number = scanner.scan(/\d+/)
    number.to_i
    scanner.skip(",")
  end
end

def scan_integer(source)
  scanner = StringScanner.new(source)
  while scanner.scan_integer
    scanner.skip(",")
  end
end

Benchmark.ips do |x|
  x.report("scan.to_i") { scan_to_i(source) }
  x.report("scan_integer") { scan_integer(source) }
  x.compare!
end
```

Before:

```
ruby 3.3.4 (2024-07-09 revision be1089c8ec) +YJIT [arm64-darwin23]
Warming up --------------------------------------
           scan.to_i    93.000 i/100ms
        scan_integer   232.000 i/100ms
Calculating -------------------------------------
           scan.to_i    933.191 (± 0.2%) i/s    (1.07 ms/i) -      4.743k in   5.082597s
        scan_integer      2.326k (± 0.8%) i/s  (429.99 μs/i) -     11.832k in   5.087974s

Comparison:
        scan_integer:     2325.6 i/s
           scan.to_i:      933.2 i/s - 2.49x  slower
```

After:

```
ruby 3.3.4 (2024-07-09 revision be1089c8ec) +YJIT [arm64-darwin23]
Warming up --------------------------------------
           scan.to_i    96.000 i/100ms
        scan_integer   274.000 i/100ms
Calculating -------------------------------------
           scan.to_i    969.489 (± 0.2%) i/s    (1.03 ms/i) -      4.896k in   5.050114s
        scan_integer      2.756k (± 0.1%) i/s  (362.88 μs/i) -     13.974k in   5.070837s

Comparison:
        scan_integer:     2755.8 i/s
           scan.to_i:      969.5 i/s - 2.84x  slower
```
  • Loading branch information
byroot committed Nov 27, 2024
1 parent eb875ea commit e92e6d1
Showing 1 changed file with 38 additions and 3 deletions.
41 changes: 38 additions & 3 deletions ext/strscan/strscan.c
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ static VALUE StringScanner;
static VALUE ScanError;
static ID id_byteslice;

static int usascii_encindex, utf8_encindex, binary_encindex;

struct strscanner
{
/* multi-purpose flags */
Expand Down Expand Up @@ -683,6 +685,14 @@ strscan_search(regex_t *reg, VALUE str, struct re_registers *regs, void *args_pt
ONIG_OPTION_NONE);
}

static void
strscan_enc_check(VALUE str1, VALUE str2)
{
if (ENCODING_GET_INLINED(str1) != ENCODING_GET_INLINED(str2)) {
rb_enc_check(str1, str2);
}
}

static VALUE
strscan_do_scan(VALUE self, VALUE pattern, int succptr, int getstr, int headonly)
{
Expand Down Expand Up @@ -710,18 +720,21 @@ strscan_do_scan(VALUE self, VALUE pattern, int succptr, int getstr, int headonly
}
else {
StringValue(pattern);
rb_encoding *enc = rb_enc_check(p->str, pattern);
if (S_RESTLEN(p) < RSTRING_LEN(pattern)) {
strscan_enc_check(p->str, pattern);
return Qnil;
}

if (headonly) {
strscan_enc_check(p->str, pattern);

if (memcmp(CURPTR(p), RSTRING_PTR(pattern), RSTRING_LEN(pattern)) != 0) {
return Qnil;
}
set_registers(p, RSTRING_LEN(pattern));
}
else {
rb_encoding *enc = rb_enc_check(p->str, pattern);
long pos = rb_memsearch(RSTRING_PTR(pattern), RSTRING_LEN(pattern),
CURPTR(p), S_RESTLEN(p), enc);
if (pos == -1) {
Expand Down Expand Up @@ -1282,6 +1295,24 @@ strscan_parse_integer(struct strscanner *p, int base, long len)
return integer;
}

static inline bool
strscan_ascii_compat_fastpath(VALUE str) {
int encindex = ENCODING_GET_INLINED(str);
// The overwhelming majority of strings are in one of these 3 encodings.
return encindex == utf8_encindex || encindex == binary_encindex || encindex == usascii_encindex;
}

static inline void
strscan_must_ascii_compat(VALUE str)
{
// The overwhelming majority of strings are in one of these 3 encodings.
if (RB_LIKELY(strscan_ascii_compat_fastpath(str))) {
return;
}

rb_must_asciicompat(str);
}

static VALUE
strscan_scan_base10_integer(VALUE self)
{
Expand All @@ -1292,7 +1323,7 @@ strscan_scan_base10_integer(VALUE self)
GET_SCANNER(self, p);
CLEAR_MATCH_STATUS(p);

rb_must_asciicompat(p->str);
strscan_must_ascii_compat(p->str);

ptr = CURPTR(p);

Expand Down Expand Up @@ -1330,7 +1361,7 @@ strscan_scan_base16_integer(VALUE self)
GET_SCANNER(self, p);
CLEAR_MATCH_STATUS(p);

rb_must_asciicompat(p->str);
strscan_must_ascii_compat(p->str);

ptr = CURPTR(p);

Expand Down Expand Up @@ -2251,6 +2282,10 @@ Init_strscan(void)

id_byteslice = rb_intern("byteslice");

usascii_encindex = rb_usascii_encindex();
utf8_encindex = rb_utf8_encindex();
binary_encindex = rb_ascii8bit_encindex();

StringScanner = rb_define_class("StringScanner", rb_cObject);
ScanError = rb_define_class_under(StringScanner, "Error", rb_eStandardError);
if (!rb_const_defined(rb_cObject, id_scanerr)) {
Expand Down

0 comments on commit e92e6d1

Please sign in to comment.