diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index c9484b5..0000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,16 +0,0 @@ -name: ci -on: [push] -jobs: - test: - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - ruby: [2.0, 2.1, 2.2, 2.3, 2.4, 2.5, 2.6, 2.7, head] - steps: - - uses: actions/checkout@v2 - - uses: ruby/setup-ruby@v1 - with: - ruby-version: ${{ matrix.ruby }} - bundler-cache: true # bundle install - - run: bundle exec rake diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..e9793d5 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,20 @@ +name: test + +on: [push] + +jobs: + test: + strategy: + max-parallel: 3 + matrix: + os: [macos, ubuntu] + ruby: [3.0, 3.4] + runs-on: ${{ matrix.os }}-latest + steps: + - uses: actions/checkout@v3 + - uses: taiki-e/install-action@just + - uses: ruby/setup-ruby@v1 + with: + bundler-cache: true + ruby-version: ${{ matrix.ruby }} + - run: just ci diff --git a/.gitignore b/.gitignore index 313c3b8..97893e8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ -*.gem -.ruby-version -Gemfile.lock -lib/*.so -lib/kdtree.bundle -tmp +*.bundle +*.so +/.tool-versions +/Gemfile.lock +/pkg/ +/tmp/ diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 0000000..8a770a7 --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,59 @@ +require: + - standard + +inherit_gem: + standard: config/ruby-3.3.yml + standard-custom: config/base.yml + standard-performance: config/base.yml + +plugins: + - standard-custom + - standard-performance + - rubocop-performance + +AllCops: + NewCops: enable + SuggestExtensions: false + +# +# fight with standardrb! +# + +# we like these, don't remove +Bundler/OrderedGems: { Enabled: true } # sort gems in gemfile +Bundler/GemVersion: { Enabled: true } # make sure we have versions +Layout/EmptyLineBetweenDefs: { AllowAdjacentOneLineDefs: true } +Lint/NonLocalExitFromIterator: { Enabled: false } +Lint/RedundantDirGlobSort: { Enabled: true } # glob is already sorted +Performance/RegexpMatch: { Enabled: false } +Style/HashSyntax: { EnforcedShorthandSyntax: always } # use modern hash syntax +Style/NestedTernaryOperator: { Enabled: false } # we do this sometimes +Style/NonNilCheck: { Enabled: false } # allow x != nil for clarity +Style/RedundantAssignment: { Enabled: false } # allows s=xxx;s=yyy;s +Style/RedundantReturn: { Enabled: false } # sometines we do this while working on something +Style/StringConcatenation: { Enabled: true } # prefer interpolation +Style/TrailingCommaInArrayLiteral: { EnforcedStyleForMultiline: consistent_comma } # commas!! +Style/TrailingCommaInHashLiteral: { EnforcedStyleForMultiline: consistent_comma } # commas!! + +# +# These are rules that are not enabled by default (in standardrb) but we tend to +# write code this way. We don't often trigger these, but it matches our style. +# + +Lint/MissingSuper: { Enabled: true } +Naming/FileName: { Enabled: true } +Naming/MemoizedInstanceVariableName: { Enabled: true } +Naming/MethodName: { Enabled: true } +Performance/MapCompact: { Enabled: true } +Performance/SelectMap: { Enabled: true } +Style/BlockDelimiters: { Enabled: true } +Style/CollectionCompact: { Enabled: true } +Style/CollectionMethods: { Enabled: true } +Style/HashEachMethods: { Enabled: true } +Style/HashTransformKeys: { Enabled: true } +Style/HashTransformValues: { Enabled: true } +Style/MinMax: { Enabled: true } +Style/PreferredHashMethods: { Enabled: true } +Style/SelectByRegexp: { Enabled: true } +Style/SymbolArray: { Enabled: true } +Style/WordArray: { Enabled: true } diff --git a/Gemfile b/Gemfile index 1aa98e4..3c2da76 100644 --- a/Gemfile +++ b/Gemfile @@ -1,2 +1,9 @@ source "http://rubygems.org" gemspec + +group :development, :test do + gem "minitest", "~> 5.25" + gem "rake", "~> 13.2" + gem "rake-compiler", "~> 1.3" + gem "standard", "~> 1.49", require: false, platform: :mri +end diff --git a/LICENSE b/LICENSE index 06f5dd4..95d882d 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2012 Adam Doppelt +Copyright (c) 2025 Adam Doppelt Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/README.md b/README.md index a60efa9..24d5f74 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,26 @@ -## Kdtree +## Kdtree [![test](https://github.com/gurgeous/kdtree/actions/workflows/test.yml/badge.svg)](https://github.com/gurgeous/kdtree/actions/workflows/test.yml) -[![Build Status](https://github.com/gurgeous/kdtree/workflows/ci/badge.svg?branch=master)](https://github.com/gurgeous/kdtree/actions) A kd tree is a data structure that recursively partitions the world in order to rapidly answer nearest neighbor queries. A generic kd tree can support any number of dimensions, and can return either the nearest neighbor or a set of N nearest neighbors. This gem is a blazingly fast, native, 2d kdtree. It's specifically built to find the nearest neighbor when searching millions of points. It's used in production at Urbanspoon and several other companies. -The first version of this gem was released back in 2009. See the original [blog post](http://gurge.com/2009/10/22/ruby-nearest-neighbor-fast-kdtree-gem/) for the full story. Wikipedia has a great [article on kdtrees](http://en.wikipedia.org/wiki/K-d_tree). +The first version of this gem was released back in 2009. Wikipedia has a great [article on kdtrees](http://en.wikipedia.org/wiki/K-d_tree). -Note: kdtree 0.3 obsoletes these forks: ghazel-kdtree, groupon-kdtree, tupalo-kdree. Thanks guys! +Note: kdtree obsoletes these forks: ghazel-kdtree, groupon-kdtree, tupalo-kdree. Thanks guys! -### Usage +### Installation -First, install kdtree: +```ruby +# install gem +$ gem install kdtree -```sh -$ sudo gem install kdtree +# or add to your Gemfile +gem "kdtree" ``` +### Usage + It's easy to use: - **Kdtree.new(points)** - construct a new tree. Each point should be of the form `[x, y, id]`, where `x/y` are floats and `id` is an int. Not a string, not an object, **just an int**. @@ -50,17 +53,17 @@ kd2 = File.open("treefile") { |f| Kdtree.new(f) } ### Performance -Kdtree is fast. How fast? Using a tree with 1 million points on my i5 2.8ghz: +Kdtree is fast. How fast? Using a tree with 1 million points on my M1: ``` -build (init) 3.52s -nearest point 0.000003s -nearest 5 points 0.000004s -nearest 50 points 0.000014s -nearest 255 points 0.000063s - -persist 0.301963s -read (init) 0.432676s +build (init) 0.96s +persist 0.000814s +read (init) 0.009236s + +nearest point 0.000002s +nearest 5 points 0.000002s +nearest 50 points 0.000006s +nearest 255 points 0.000026s ``` ### Limitations @@ -81,9 +84,16 @@ Since this gem was originally released, several folks have contributed important ### Changelog -Note: This gem is stable, maintained and continues to work great with all modern versions of Ruby MRI. Our CI tests through Ruby 2.7. No need for new releases until something breaks! +Note: This gem is stable, maintained and continues to work great with all modern versions of Ruby MRI. Our CI tests through Ruby 3.4. No need for new releases until something breaks! + +#### 0.5 - May 2025 + +- justfile +- hygiene - updated deps, format/lint, modernize rakefile +- moved to ruby 3.x or higher, tested with ruby 3.4 +- updated benchmark numbers (still real fast) -#### 0.4 - current +#### 0.4 - Mar 2017 - this is mostly housekeeping - test on more rubies, fix a few warnings diff --git a/Rakefile b/Rakefile index 65bd445..358393f 100644 --- a/Rakefile +++ b/Rakefile @@ -1,41 +1,12 @@ -require "bundler/setup" +require "bundler/gem_tasks" require "rake/extensiontask" require "rake/testtask" -# load the spec, we use it below -spec = Gem::Specification.load("kdtree.gemspec") - -# -# gem -# - -task :build do - system "gem build --quiet kdtree.gemspec" -end - -task install: :build do - system "sudo gem install --quiet kdtree-#{spec.version}.gem" -end - -task release: :build do - system "git tag -a #{spec.version} -m 'Tagging #{spec.version}'" - system "git push --tags" - system "gem push kdtree-#{spec.version}.gem" -end - -# -# rake-compiler -# - -Rake::ExtensionTask.new("kdtree", spec) - - -# -# testing -# +Rake::ExtensionTask.new("kdtree") +task test: :compile -Rake::TestTask.new(:test) do |test| - test.libs << "test" +Rake::TestTask.new do + _1.libs << "test" + _1.test_files = FileList["test/**/test_*.rb"] end -task test: :compile task default: :test diff --git a/ext/kdtree/kdtree.c b/ext/kdtree/kdtree.c index 477d65a..6f1c005 100644 --- a/ext/kdtree/kdtree.c +++ b/ext/kdtree/kdtree.c @@ -5,16 +5,14 @@ // // the tree itself -typedef struct kdtree_data -{ +typedef struct kdtree_data { int root; int len; struct kdtree_node *nodes; } kdtree_data; // a node in the tree -typedef struct kdtree_node -{ +typedef struct kdtree_node { float x, y; int id; int left; @@ -28,7 +26,7 @@ typedef struct kresult { } kresult; // helper macro for digging out our struct -#define KDTREEP \ +#define KDTREEP \ struct kdtree_data *kdtreep; \ Data_Get_Struct(kdtree, struct kdtree_data, kdtreep); @@ -43,8 +41,10 @@ static VALUE kdtree_to_s(VALUE kdtree); // kdtree helpers static int kdtree_build(struct kdtree_data *kdtreep, int min, int max, int depth); -static void kdtree_nearest0(struct kdtree_data *kdtreep, int i, float x, float y, int depth, int *n_index, float *n_dist); -static void kdtree_nearestk0(struct kdtree_data *kdtreep, int i, float x, float y, int k, int depth, kresult *k_list, int *k_len, float *k_dist); +static void kdtree_nearest0(struct kdtree_data *kdtreep, int i, float x, float y, int depth, + int *n_index, float *n_dist); +static void kdtree_nearestk0(struct kdtree_data *kdtreep, int i, float x, float y, int k, int depth, + kresult *k_list, int *k_len, float *k_dist); // io helpers static void read_all(VALUE io, void *buf, int len); @@ -59,16 +59,14 @@ static ID id_read, id_write, id_binmode; // implementation // -static VALUE kdtree_alloc(VALUE klass) -{ +static VALUE kdtree_alloc(VALUE klass) { struct kdtree_data *kdtreep; VALUE obj = Data_Make_Struct(klass, struct kdtree_data, 0, kdtree_free, kdtreep); kdtreep->root = -1; return obj; } -static void kdtree_free(struct kdtree_data *kdtreep) -{ +static void kdtree_free(struct kdtree_data *kdtreep) { if (kdtreep) { free(kdtreep->nodes); } @@ -95,8 +93,7 @@ static void kdtree_free(struct kdtree_data *kdtreep) * kdtree. This makes it possible to build the tree in advance, persist it, and * start it up quickly later. See persist for more information. */ -static VALUE kdtree_initialize(VALUE kdtree, VALUE arg) -{ +static VALUE kdtree_initialize(VALUE kdtree, VALUE arg) { KDTREEP; if (TYPE(arg) == T_ARRAY) { @@ -147,23 +144,20 @@ static VALUE kdtree_initialize(VALUE kdtree, VALUE arg) return kdtree; } -static int comparex(const void *pa, const void *pb) -{ - float a = ((const struct kdtree_node*)pa)->x; - float b = ((const struct kdtree_node*)pb)->x; +static int comparex(const void *pa, const void *pb) { + float a = ((const struct kdtree_node *)pa)->x; + float b = ((const struct kdtree_node *)pb)->x; return (a < b) ? -1 : ((a > b) ? 1 : 0); } -static int comparey(const void *pa, const void *pb) -{ - float a = ((const struct kdtree_node*)pa)->y; - float b = ((const struct kdtree_node*)pb)->y; +static int comparey(const void *pa, const void *pb) { + float a = ((const struct kdtree_node *)pa)->y; + float b = ((const struct kdtree_node *)pb)->y; return (a < b) ? -1 : ((a > b) ? 1 : 0); } -static int kdtree_build(struct kdtree_data *kdtreep, int min, int max, int depth) -{ - int(*compar)(const void *, const void *); +static int kdtree_build(struct kdtree_data *kdtreep, int min, int max, int depth) { + int (*compar)(const void *, const void *); struct kdtree_node *m; int median; if (max <= min) { @@ -198,8 +192,7 @@ static int kdtree_build(struct kdtree_data *kdtreep, int min, int max, int depth * # which city is closest to Boston? * kd.nearest(42.4, -71.1) #=> 2 */ -static VALUE kdtree_nearest(VALUE kdtree, VALUE x, VALUE y) -{ +static VALUE kdtree_nearest(VALUE kdtree, VALUE x, VALUE y) { int n_index; float n_dist; KDTREEP; @@ -214,8 +207,8 @@ static VALUE kdtree_nearest(VALUE kdtree, VALUE x, VALUE y) return INT2NUM((kdtreep->nodes + n_index)->id); } -static void kdtree_nearest0(struct kdtree_data *kdtreep, int i, float x, float y, int depth, int *n_index, float *n_dist) -{ +static void kdtree_nearest0(struct kdtree_data *kdtreep, int i, float x, float y, int depth, + int *n_index, float *n_dist) { struct kdtree_node *n; float ad; int near, far; @@ -234,11 +227,13 @@ static void kdtree_nearest0(struct kdtree_data *kdtreep, int i, float x, float y // if (ad <= 0) { - near = n->left; far = n->right; + near = n->left; + far = n->right; } else { - near = n->right; far = n->left; + near = n->right; + far = n->left; } - kdtree_nearest0(kdtreep, near, x, y, depth + 1, n_index, n_dist); + kdtree_nearest0(kdtreep, near, x, y, depth + 1, n_index, n_dist); if (ad * ad < *n_dist) { kdtree_nearest0(kdtreep, far, x, y, depth + 1, n_index, n_dist); } @@ -280,9 +275,9 @@ static void kdtree_nearest0(struct kdtree_data *kdtreep, int i, float x, float y * # which two cities are closest to San Francisco? * kd.nearestk(34.1, -118.2, 2) #=> [2, 1] */ -static VALUE kdtree_nearestk(VALUE kdtree, VALUE x, VALUE y, VALUE k) -{ - // note I leave an extra slot here at the end because of the way our binary insert works +static VALUE kdtree_nearestk(VALUE kdtree, VALUE x, VALUE y, VALUE k) { + // note I leave an extra slot here at the end because of the way our binary + // insert works kresult k_list[MAX_K + 1]; int k_len = 0; float k_dist = INT_MAX; @@ -296,7 +291,8 @@ static VALUE kdtree_nearestk(VALUE kdtree, VALUE x, VALUE y, VALUE k) } else if (ki > MAX_K) { ki = MAX_K; } - kdtree_nearestk0(kdtreep, kdtreep->root, NUM2DBL(x), NUM2DBL(y), ki, 0, k_list, &k_len, &k_dist); + kdtree_nearestk0(kdtreep, kdtreep->root, NUM2DBL(x), NUM2DBL(y), ki, 0, k_list, &k_len, + &k_dist); // convert result to ruby array ary = rb_ary_new(); @@ -306,8 +302,8 @@ static VALUE kdtree_nearestk(VALUE kdtree, VALUE x, VALUE y, VALUE k) return ary; } -static void kdtree_nearestk0(struct kdtree_data *kdtreep, int i, float x, float y, int k, int depth, kresult *k_list, int *k_len, float *k_dist) -{ +static void kdtree_nearestk0(struct kdtree_data *kdtreep, int i, float x, float y, int k, int depth, + kresult *k_list, int *k_len, float *k_dist) { struct kdtree_node *n; float ad; int near, far; @@ -327,11 +323,13 @@ static void kdtree_nearestk0(struct kdtree_data *kdtreep, int i, float x, float // if (ad <= 0) { - near = n->left; far = n->right; + near = n->left; + far = n->right; } else { - near = n->right; far = n->left; + near = n->right; + far = n->left; } - kdtree_nearestk0(kdtreep, near, x, y, k, depth + 1, k_list, k_len, k_dist); + kdtree_nearestk0(kdtreep, near, x, y, k, depth + 1, k_list, k_len, k_dist); if (ad * ad < *k_dist) { kdtree_nearestk0(kdtreep, far, x, y, k, depth + 1, k_list, k_len, k_dist); } @@ -404,8 +402,7 @@ static void kdtree_nearestk0(struct kdtree_data *kdtreep, int i, float x, float * # later, read the tree from disk * kd2 = File.open("treefile") { |f| Kdtree.new(f) } */ -static VALUE kdtree_persist(VALUE kdtree, VALUE io) -{ +static VALUE kdtree_persist(VALUE kdtree, VALUE io) { KDTREEP; if (!rb_respond_to(io, rb_intern("write"))) { @@ -427,12 +424,11 @@ static VALUE kdtree_persist(VALUE kdtree, VALUE io) * * A string that tells you a bit about the tree. */ -static VALUE kdtree_to_s(VALUE kdtree) -{ +static VALUE kdtree_to_s(VALUE kdtree) { char buf[256]; KDTREEP; - sprintf(buf, "#<%s:%p nodes=%d>", rb_obj_classname(kdtree), (void*)kdtree, kdtreep->len); + sprintf(buf, "#<%s:%p nodes=%d>", rb_obj_classname(kdtree), (void *)kdtree, kdtreep->len); return rb_str_new(buf, strlen(buf)); } @@ -440,8 +436,7 @@ static VALUE kdtree_to_s(VALUE kdtree) // io helpers // -static void read_all(VALUE io, void *buf, int len) -{ +static void read_all(VALUE io, void *buf, int len) { VALUE string = rb_funcall(io, id_read, 1, INT2NUM(len)); if (NIL_P(string) || RSTRING_LEN(string) != len) { rb_raise(rb_eEOFError, "end of file reached"); @@ -449,8 +444,7 @@ static void read_all(VALUE io, void *buf, int len) memcpy(buf, RSTRING_PTR(string), len); } -static void write_all(VALUE io, const void *buf, int len) -{ +static void write_all(VALUE io, const void *buf, int len) { rb_funcall(io, id_write, 1, rb_str_new(buf, len)); } @@ -486,8 +480,7 @@ static void write_all(VALUE io, const void *buf, int len) * * http://en.wikipedia.org/wiki/Kd-tree */ -void Init_kdtree() -{ +void Init_kdtree() { static VALUE clazz; clazz = rb_define_class("Kdtree", rb_cObject); diff --git a/justfile b/justfile new file mode 100644 index 0000000..2fa9dce --- /dev/null +++ b/justfile @@ -0,0 +1,58 @@ +default: test + +# +# ci/test +# + +# check repo - lint & test +check: lint test + +# for ci. don't bother linting on windows +ci: + @just test + +# run tests +test *ARGS: + @just _banner rake test {{ARGS}} + @bundle exec rake test {{ARGS}} + +# +# build/release +# + +clean: + rm -rf pkg tmp lib/*.bundle lib/*.so + +gem-build: check clean + @just _banner rake build... + @bundle exec rake build + +# this will tag, build and push to rubygems +gem-push: check clean + @just _banner rake release... + rake release + +# +# lint +# + +# format with rubocop +format: (lint "-a") + +# lint with rubocop +lint *ARGS: + @just _banner lint... + bundle exec rubocop {{ARGS}} + + +# +# util +# + +_banner *ARGS: (_message BG_GREEN ARGS) +_warning *ARGS: (_message BG_YELLOW ARGS) +_fatal *ARGS: (_message BG_RED ARGS) + @exit 1 +_message color *ARGS: + @msg=$(printf "[%s] %s" $(date +%H:%M:%S) "{{ARGS}}") ; \ + printf "{{color+BOLD+WHITE}}%-72s{{ NORMAL }}\n" "$msg" diff --git a/kdtree.gemspec b/kdtree.gemspec index 165695a..27264e6 100644 --- a/kdtree.gemspec +++ b/kdtree.gemspec @@ -1,23 +1,33 @@ Gem::Specification.new do |s| - s.name = "kdtree" - s.version = "0.4" + s.name = "kdtree" + s.version = "0.5" + s.authors = ["Adam Doppelt"] + s.email = "amd@gurge.com" - s.authors = ["Adam Doppelt"] - s.email = ["amd@gurge.com"] - s.homepage = "http://github.com/gurgeous/kdtree" - s.license = "MIT" - s.summary = "Blazingly fast, native 2d kdtree." - s.description = < s.homepage, + "rubygems_mfa_required" => "true", + "source_code_uri" => s.homepage, + } - s.add_development_dependency "minitest", "~> 5.0" - s.add_development_dependency "rake-compiler", "~> 1.0" + s.files = %w[ + ext/kdtree/extconf.rb + ext/kdtree/kdtree.c + kdtree.gemspec + lib/kdtree.rb + LICENSE + README.md + ] - s.files = `git ls-files`.split("\n") - s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") s.extensions = ["ext/kdtree/extconf.rb"] s.require_paths = ["lib"] end diff --git a/test/test_kdtree.rb b/test/test_kdtree.rb index 3aa8148..d24b67f 100644 --- a/test/test_kdtree.rb +++ b/test/test_kdtree.rb @@ -2,6 +2,7 @@ require "kdtree" require "tempfile" require "minitest/autorun" +require "minitest/pride" # # create a tree @@ -28,7 +29,7 @@ def test_nearest kdpt = @points[id] # slow search - sortpt = @points.sort_by { |i| distance(i, pt) }.first + sortpt = @points.min_by { distance(_1, pt) } # assert kdd = distance(kdpt, pt) @@ -46,7 +47,7 @@ def test_nearestk kdpt = @points[list.last] # slow search - sortpt = @points.sort_by { |i| distance(i, pt) }[list.length - 1] + sortpt = @points.sort_by { distance(_1, pt) }[list.length - 1] # assert kdd = distance(kdpt, pt) @@ -82,7 +83,7 @@ def test_eof bytes = File.read(TMP) [2, 10, 100].each do |len| - File.open(TMP, "w") { |f| f.write(bytes[0, len]) } + File.write(TMP, bytes[0, len]) assert_raises EOFError do File.open(TMP, "r") { |f| Kdtree.new(f) } end @@ -95,6 +96,11 @@ def dont_test_speed sizes.each do |s| points = (0...s).map { |i| [rand_coord, rand_coord, i] } + puts + puts "x" * 72 + puts "x with #{s} points" + puts "x" * 72 + # build Benchmark.bm(17) do |bm| kdtree = nil @@ -120,7 +126,6 @@ def dont_test_speed end end end - puts end end @@ -136,13 +141,15 @@ def rand_coord end end -# running dont_test_speed on my i5 2.8ghz: +# +# running dont_test_speed on my M1: # # user system total real -# build 3.350000 0.020000 3.370000 ( 3.520528) -# persist 0.150000 0.020000 0.170000 ( 0.301963) -# read 0.280000 0.000000 0.280000 ( 0.432676) -# 100 queries (1) 0.000000 0.000000 0.000000 ( 0.000319) -# 100 queries (5) 0.000000 0.000000 0.000000 ( 0.000412) -# 100 queries (50) 0.000000 0.000000 0.000000 ( 0.001417) -# 100 queries (255) 0.000000 0.000000 0.000000 ( 0.006268) +# build 0.954846 0.008057 0.962903 ( 0.999617) +# persist 0.000843 0.005116 0.005959 ( 0.007224) +# read 0.008995 0.003483 0.012478 ( 0.012649) +# 100 queries (1) 0.000176 0.000011 0.000187 ( 0.000186) +# 100 queries (5) 0.000217 0.000009 0.000226 ( 0.000225) +# 100 queries (50) 0.000631 0.000008 0.000639 ( 0.000638) +# 100 queries (255) 0.002591 0.000035 0.002626 ( 0.002627) +#