Skip to content

Commit

Permalink
Eliminate ~540MB of allocations from benchmark
Browse files Browse the repository at this point in the history
- Don't use def_delegators on methods called frequently, it allocates *args
- Build native extension String.strncmp to compare two substrings, which
  results in faster StringPtr#== and StringPtr#start_with?
  • Loading branch information
kputnam committed Jul 19, 2019
1 parent f9a44aa commit d06579e
Show file tree
Hide file tree
Showing 24 changed files with 326 additions and 561 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
build/generated
.bundle
.ruby-version
lib/strncmp
ext/strncmp/Makefile
ext/strncmp/strncmp.h
ext/strncmp/strncmp.o
2 changes: 2 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ before_install:
bundle update --bundler;
fi

before_script: bundle exec rake compile

rvm:
# 2.0.0
- 2.1.0
Expand Down
8 changes: 5 additions & 3 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ gem "cantor", "~> 1.2.1"

group :development do
gem "rake"
gem "rake-compiler"
gem "term-ansicolor"

gem "rspec", "3.8.0"
Expand All @@ -16,12 +17,13 @@ group :development do
gem "simplecov", :platforms => [:ruby_24, :ruby_25]
gem "simplecov-inline-html", :platforms => [:ruby_24, :ruby_25]

# gem "stackprof"
gem "stackprof"
# gem "fasterer"
# gem "benchmark-ips"
# gem "memory_profiler"
gem "benchmark-ips"
gem "memory_profiler", :platform => [:ruby_23, :ruby_24, :ruby_25]
# gem "allocation_stats"
# gem "heapy"
# gem "derailed_benchmarks"

# We're using a patched version installed in yard/ until the
# maintainer improves the plugin. The patch has been submitted
Expand Down
11 changes: 10 additions & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
GEM
remote: http://rubygems.org/
specs:
benchmark-ips (2.7.2)
cantor (1.2.1)
diff-lcs (1.3)
docile (1.3.1)
json (2.1.0)
memory_profiler (0.9.13)
rake (12.3.2)
rake-compiler (1.0.7)
rake
rspec (3.8.0)
rspec-core (~> 3.8.0)
rspec-expectations (~> 3.8.0)
Expand All @@ -27,6 +31,7 @@ GEM
simplecov-html (~> 0.10.0)
simplecov-html (0.10.2)
simplecov-inline-html (0.0.1)
stackprof (0.2.12)
term-ansicolor (1.7.1)
tins (~> 1.0)
tins (1.20.2)
Expand All @@ -37,14 +42,18 @@ PLATFORMS
ruby

DEPENDENCIES
benchmark-ips
cantor (~> 1.2.1)
memory_profiler
rake
rake-compiler
rspec (= 3.8.0)
rspec-collection_matchers
simplecov
simplecov-inline-html
stackprof
term-ansicolor
yard (~> 0.9.12)

BUNDLED WITH
1.17.2
1.17.3
18 changes: 15 additions & 3 deletions Rakefile
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
require "pathname"
abspath = Pathname.new(File.dirname(__FILE__)).expand_path
relpath = abspath.relative_path_from(Pathname.pwd)
require "bundler"
Bundler::GemHelper.install_tasks

task :default => :spec

Expand All @@ -18,12 +17,25 @@ end

# Note options are loaded from .yardopts
require "yard"
require "pathname"
abspath = Pathname.new(File.dirname(__FILE__)).expand_path
relpath = abspath.relative_path_from(Pathname.pwd)
YARD::Rake::YardocTask.new(:yard => :clobber_yard)
task :clobber_yard do

rm_rf "#{relpath}/build/generated/doc"
mkdir_p "#{relpath}/build/generated/doc/images"
end

task :console do
exec(*%w(irb -I lib -r stupidedi))
end

if RUBY_PLATFORM !~ /java/
require "rake/extensiontask"
gemspec = Gem::Specification.load("stupidedi.gemspec")

Rake::ExtensionTask.new("strncmp") do |x|
x.lib_dir = "lib/strncmp"
end
end
6 changes: 6 additions & 0 deletions ext/strncmp/extconf.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
require "mkmf"
extension_name = "strncmp/strncmp"

create_header
dir_config extension_name
create_makefile extension_name
34 changes: 34 additions & 0 deletions ext/strncmp/strncmp.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#include "ruby.h"
#include "extconf.h"

VALUE rb_strncmp(VALUE self, VALUE str1, VALUE offset1, VALUE str2, VALUE offset2, VALUE length) {
// rb_raise(rb_eArgError, "message %s", "argument");

if (!FIXNUM_P(length)) rb_raise(rb_eTypeError, "length must be an Integer");
if (!FIXNUM_P(offset1)) rb_raise(rb_eTypeError, "offset1 must be an Integer");
if (!FIXNUM_P(offset2)) rb_raise(rb_eTypeError, "offset1 must be an Integer");

long len = NUM2LONG(length);
long beg1 = NUM2LONG(offset1);
long beg2 = NUM2LONG(offset2);

if (len < 0) rb_raise(rb_eArgError, "length cannot be negative");
if (beg1 < 0) rb_raise(rb_eArgError, "offset1 cannot be negative");
if (beg2 < 0) rb_raise(rb_eArgError, "offset2 cannot be negative");

if (beg1 + len > rb_str_strlen(str1)) return Qnil;
if (beg2 + len > rb_str_strlen(str2)) return Qnil;

char *ptr1 = rb_str_subpos(str1, beg1, &len);
char *ptr2 = rb_str_subpos(str2, beg2, &len);

if (ptr1 == 0 || ptr2 == 0) return Qnil;

return (memcmp(ptr1, ptr2, len) == 0) ? Qtrue : Qfalse;
}

void Init_strncmp()
{
// VALUE rb_cString = rb_define_class("String", rb_cObject);
rb_define_singleton_method(rb_cString, "strncmp", rb_strncmp, 5);
}
11 changes: 11 additions & 0 deletions lib/ruby/string.rb
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,17 @@ def position
def join
gsub(/\n[ \t]+/, " ")
end

unless "".respond_to?(:match?)
def match?(pattern, pos=nil)
if pos.nil?
!!(self =~ pattern)
else
!!match(pattern, pos)
end
end
end

end
end
end
1 change: 1 addition & 0 deletions lib/strncmp.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
require "strncmp/strncmp"
4 changes: 4 additions & 0 deletions lib/stupidedi.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@

$:.unshift(File.expand_path("..", __FILE__))

if RUBY_PLATFORM !~ /java/
require "strncmp"
end

require "ruby/regexp"
require "ruby/array"
require "ruby/blank"
Expand Down
4 changes: 2 additions & 2 deletions lib/stupidedi/parser/builder_dsl.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,10 @@ class BuilderDsl
:empty?, :first?, :last?, :deterministic?

def initialize(machine, strict = true)
@position = Reader::NoPosition
@position = Reader::StacktracePosition
@machine = machine
@strict = strict
@separators = Reader::Separators.empty
@separators = Reader::Separators.blank
@segment_dict = Reader::SegmentDict.empty
end

Expand Down
2 changes: 1 addition & 1 deletion lib/stupidedi/parser/generation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ module Generation
# @yield [Reader::IgnoredTok]
# @return [(StateMachine, Reader::Tokenizer::Result)]
def read(tokenizer, options = {})
limit = options.fetch(:nondeterminism, 1)
#imit = options.fetch(:nondeterminism, 1)
machine = self.dup

return machine, tokenizer.each do |token|
Expand Down
2 changes: 1 addition & 1 deletion lib/stupidedi/parser/states/initial_state.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ class << InitialState
# @return [InitialState]
def build(zipper)
new(
Reader::Separators.empty,
Reader::Separators.blank,
Reader::SegmentDict.empty,

InstructionTable.build(
Expand Down
45 changes: 42 additions & 3 deletions lib/stupidedi/reader/input.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,36 @@ class Input
# @return [Position]
attr_reader :position

def_delegators :@pointer, :defined_at?, :empty?, :count, :head, :take,
:match?, :slice, :index, :rindex, :reify, :last, :length, :=~, :[]
def_delegators :@pointer, :count, :take, :match?, :slice,
:index, :rindex, :reify, :last, :length, :=~, :[]

def initialize(pointer, position)
@pointer, @position =
pointer, position
end

# NOTE: This could be implemented using def_delegators, but unfortunately
# that allocates an Array to pass the arguments along. Since this method
# is called frequently, we can reduce allocations by defining it this way.
def head
@pointer.head
end

# NOTE: This could be implemented using def_delegators, but unfortunately
# that allocates an Array to pass the arguments along. Since this method
# is called frequently, we can reduce allocations by defining it this way.
def defined_at?(n)
@pointer.defined_at?(n)
end

# NOTE: This could be implemented using def_delegators, but unfortunately
# that allocates an Array to pass the arguments along. Since this method
# is called frequently, we can reduce allocations by defining it this way.
# NOTE:
def empty?
@pointer.empty?
end

# @NOTE: This allocates 3 objects: Input, Position, and StringPtr
def tail
drop(1)
Expand All @@ -28,7 +50,11 @@ def tail
# @NOTE: This allocates 3 objects: Input, Position, and StringPtr, so
# if you've written x.drop(10).position.. x.position_at(10) is cheaper
def drop(n)
self.class.new(@pointer.drop(n), position_at(n))
if n.zero?
self
else
self.class.new(@pointer.drop(n), position_at(n))
end
end

# NOTE: This allocates 2 objects: StringPtr and Position
Expand All @@ -41,6 +67,19 @@ def position_at(n)
@position.advance(@pointer.take(n))
end
end

def start_with?(other)
other and @pointer.start_with?(other)
end

def skip_control_characters(offset = 0)
while @pointer.defined_at?(offset) \
and Reader.is_control_character?(@pointer[offset])
offset += 1
end

drop(offset)
end
end

class << Input
Expand Down
Loading

0 comments on commit d06579e

Please sign in to comment.