2019-12-06 15:03:29 -05:00
|
|
|
// String internment benchmarks and baselines
|
|
|
|
//
|
2022-05-03 14:14:29 -04:00
|
|
|
// Copyright (C) 2014-2022 Ryan Specialty Group, LLC.
|
2020-03-06 11:05:18 -05:00
|
|
|
//
|
|
|
|
// This file is part of TAME.
|
2019-12-06 15:03:29 -05:00
|
|
|
//
|
|
|
|
// This program is free software: you can redistribute it and/or modify
|
|
|
|
// it under the terms of the GNU General Public License as published by
|
|
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
|
|
// (at your option) any later version.
|
|
|
|
//
|
|
|
|
// This program is distributed in the hope that it will be useful,
|
|
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
// GNU General Public License for more details.
|
|
|
|
//
|
|
|
|
// You should have received a copy of the GNU General Public License
|
|
|
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
//
|
|
|
|
// Note that the baseline tests have a _suffix_ rather than a prefix so that
|
|
|
|
// they are still grouped with the associated test in the output, since it's
|
|
|
|
// sorted lexically by function name.
|
|
|
|
|
|
|
|
#![feature(test)]
|
|
|
|
|
|
|
|
extern crate tamer;
|
|
|
|
extern crate test;
|
|
|
|
|
|
|
|
use std::rc::Rc;
|
|
|
|
use tamer::sym::*;
|
|
|
|
use test::Bencher;
|
|
|
|
|
2019-12-09 23:13:17 -05:00
|
|
|
fn gen_strs(n: usize) -> Vec<String> {
|
|
|
|
(0..n)
|
|
|
|
.map(|n| n.to_string() + "foobarbazquuxlongsymbol")
|
|
|
|
.collect()
|
|
|
|
}
|
|
|
|
|
2019-12-23 23:26:42 -05:00
|
|
|
mod interner {
|
2019-12-06 15:03:29 -05:00
|
|
|
use super::*;
|
|
|
|
use std::collections::hash_map::RandomState;
|
|
|
|
use std::collections::HashSet;
|
|
|
|
use std::hash::BuildHasher;
|
|
|
|
|
|
|
|
pub struct HashSetSut<S = RandomState>
|
|
|
|
where
|
|
|
|
S: BuildHasher,
|
|
|
|
{
|
|
|
|
pub map: HashSet<Rc<str>, S>,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl<S> HashSetSut<S>
|
|
|
|
where
|
|
|
|
S: BuildHasher + Default,
|
|
|
|
{
|
|
|
|
#[inline]
|
|
|
|
fn new() -> Self {
|
|
|
|
Self {
|
|
|
|
map: HashSet::with_hasher(Default::default()),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn intern(&mut self, value: &str) -> Rc<str> {
|
|
|
|
if !self.map.contains(value) {
|
|
|
|
self.map.insert(value.into());
|
|
|
|
}
|
|
|
|
|
|
|
|
self.map.get(value).unwrap().clone()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// This is our baseline with a raw Rc<str>.
|
|
|
|
#[bench]
|
|
|
|
fn with_all_new_rc_str_1000_baseline(bench: &mut Bencher) {
|
|
|
|
let strs = gen_strs(1000);
|
|
|
|
|
|
|
|
bench.iter(|| {
|
|
|
|
let mut sut = HashSetSut::<RandomState>::new();
|
|
|
|
strs.iter().map(|s| sut.intern(&s)).for_each(drop);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
#[bench]
|
|
|
|
fn with_all_new_1000(bench: &mut Bencher) {
|
|
|
|
let strs = gen_strs(1000);
|
|
|
|
|
|
|
|
bench.iter(|| {
|
2021-08-02 23:54:37 -04:00
|
|
|
let sut = ArenaInterner::<RandomState, u32>::new();
|
2019-12-06 15:03:29 -05:00
|
|
|
strs.iter().map(|s| sut.intern(&s)).for_each(drop);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2021-08-13 22:54:04 -04:00
|
|
|
#[bench]
|
|
|
|
fn with_all_new_uninterned_1000(bench: &mut Bencher) {
|
|
|
|
let strs = gen_strs(1000);
|
|
|
|
|
|
|
|
bench.iter(|| {
|
|
|
|
let sut = ArenaInterner::<RandomState, u32>::new();
|
|
|
|
strs.iter().map(|s| sut.clone_uninterned(&s)).for_each(drop);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2019-12-06 15:03:29 -05:00
|
|
|
#[bench]
|
|
|
|
/// This is our baseline with a raw Rc<str>.
|
|
|
|
fn with_one_new_rc_str_1000_baseline(bench: &mut Bencher) {
|
|
|
|
bench.iter(|| {
|
|
|
|
let mut sut = HashSetSut::<RandomState>::new();
|
|
|
|
(0..1000).map(|_| sut.intern("first")).for_each(drop);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
#[bench]
|
|
|
|
fn with_one_new_1000(bench: &mut Bencher) {
|
|
|
|
bench.iter(|| {
|
2021-08-02 23:54:37 -04:00
|
|
|
let sut = ArenaInterner::<RandomState, u32>::new();
|
2019-12-06 15:03:29 -05:00
|
|
|
(0..1000).map(|_| sut.intern("first")).for_each(drop);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2020-04-29 00:48:07 -04:00
|
|
|
#[bench]
|
|
|
|
fn index_lookup_unique_1000(bench: &mut Bencher) {
|
2021-08-02 23:54:37 -04:00
|
|
|
let sut = ArenaInterner::<RandomState, u32>::new();
|
2020-04-29 00:48:07 -04:00
|
|
|
let strs = gen_strs(1000);
|
|
|
|
|
tamer: Global interners
This is a major change, and I apologize for it all being in one commit. I
had wanted to break it up, but doing so would have required a significant
amount of temporary work that was not worth doing while I'm the only one
working on this project at the moment.
This accomplishes a number of important things, now that I'm preparing to
write the first compiler frontend for TAMER:
1. `Symbol` has been removed; `SymbolId` is used in its place.
2. Consequently, symbols use 16 or 32 bits, rather than a 64-bit pointer.
3. Using symbols no longer requires dereferencing.
4. **Lifetimes no longer pollute the entire system! (`'i`)**
5. Two global interners are offered to produce `SymbolStr` with `'static`
lifetimes, simplfiying lifetime management and borrowing where strings
are still needed.
6. A nice API is provided for interning and lookups (e.g. "foo".intern())
which makes this look like a core feature of Rust.
Unfortunately, making this change required modifications to...virtually
everything. And that serves to emphasize why this change was needed:
_everything_ used symbols, and so there's no use in not providing globals.
I implemented this in a way that still provides for loose coupling through
Rust's trait system. Indeed, Rustc offers a global interner, and I decided
not to go that route initially because it wasn't clear to me that such a
thing was desirable. It didn't become apparent to me, in fact, until the
recent commit where I introduced `SymbolIndexSize` and saw how many things
had to be touched; the linker evolved so rapidly as I was trying to learn
Rust that I lost track of how bad it got.
Further, this shows how the design of the internment system was a bit
naive---I assumed certain requirements that never panned out. In
particular, everything using symbols stored `&'i Symbol<'i>`---that is, a
reference (usize) to an object containing an index (32-bit) and a string
slice (128-bit). So it was a reference to a pretty large value, which was
allocated in the arena alongside the interned string itself.
But, that was assuming that something would need both the symbol index _and_
a readily available string. That's not the case. In fact, it's pretty
clear that interning happens at the beginning of execution, that `SymbolId`
is all that's needed during processing (unless an error occurs; more on that
below); and it's not until _the very end_ that we need to retrieve interned
strings from the pool to write either to a file or to display to the
user. It was horribly wasteful!
So `SymbolId` solves the lifetime issue in itself for most systems, but it
still requires that an interner be available for anything that needs to
create or resolve symbols, which, as it turns out, is still a lot of
things. Therefore, I decided to implement them as thread-local static
variables, which is very similar to what Rustc does itself (Rustc's are
scoped). TAMER does not use threads, so the resulting `'static` lifetime
should be just fine for now. Eventually I'd like to implement `!Send` and
`!Sync`, though, to prevent references from escaping the thread (as noted in
the patch); I can't do that yet, since the feature has not yet been
stabalized.
In the end, this leaves us with a system that's much easier to use and
maintain; hopefully easier for newcomers to get into without having to deal
with so many complex lifetimes; and a nice API that makes it a pleasure to
work with symbols.
Admittedly, the `SymbolIndexSize` adds some complexity, and we'll see if I
end up regretting that down the line, but it exists for an important reason:
the `Span` and other structures that'll be introduced need to pack a lot of
data into 64 bits so they can be freely copied around to keep lifetimes
simple without wreaking havoc in other ways, but a 32-bit symbol size needed
by the linker is too large for that. (Actually, the linker doesn't yet need
32 bits for our systems, but it's going to in the somewhat near future
unless we optimize away a bunch of symbols...but I'd really rather not have
the linker hit a limit that requires a lot of code changes to resolve).
Rustc uses interned spans when they exceed 8 bytes, but I'd prefer to avoid
that for now. Most systems can just use on of the `PkgSymbolId` or
`ProgSymbolId` type aliases and not have to worry about it. Systems that
are actually shared between the compiler and the linker do, though, but it's
not like we don't already have a bunch of trait bounds.
Of course, as we implement link-time optimizations (LTO) in the future, it's
possible most things will need the size and I'll grow frustrated with that
and possibly revisit this. We shall see.
Anyway, this was exhausting...and...onward to the first frontend!
2021-08-02 23:54:37 -04:00
|
|
|
let syms = strs.iter().map(|s| sut.intern(s)).collect::<Vec<_>>();
|
2020-04-29 00:48:07 -04:00
|
|
|
|
|
|
|
bench.iter(|| {
|
|
|
|
syms.iter().map(|si| sut.index_lookup(*si)).for_each(drop);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
tamer::sym: FNV => Fx Hash
For strings of any notable length, Fx Hash outperforms FNV. Rustc also
moved to this hash function and noticed performance
improvements. Fortunately, as was accounted for in the design, this was a
trivial switch.
Here are some benchmarks to back up that claim:
test hash_set::fnv::with_all_new_1000 ... bench: 133,096 ns/iter (+/- 1,430)
test hash_set::fnv::with_all_new_1000_with_capacity ... bench: 82,591 ns/iter (+/- 592)
test hash_set::fnv::with_all_new_rc_str_1000_baseline ... bench: 162,073 ns/iter (+/- 1,277)
test hash_set::fnv::with_one_new_1000 ... bench: 37,334 ns/iter (+/- 256)
test hash_set::fnv::with_one_new_rc_str_1000_baseline ... bench: 18,263 ns/iter (+/- 261)
test hash_set::fx::with_all_new_1000 ... bench: 85,217 ns/iter (+/- 1,111)
test hash_set::fx::with_all_new_1000_with_capacity ... bench: 59,383 ns/iter (+/- 752)
test hash_set::fx::with_all_new_rc_str_1000_baseline ... bench: 98,802 ns/iter (+/- 1,117)
test hash_set::fx::with_one_new_1000 ... bench: 42,484 ns/iter (+/- 1,239)
test hash_set::fx::with_one_new_rc_str_1000_baseline ... bench: 15,000 ns/iter (+/- 233)
test hash_set::with_all_new_1000 ... bench: 137,645 ns/iter (+/- 1,186)
test hash_set::with_all_new_rc_str_1000_baseline ... bench: 163,129 ns/iter (+/- 1,725)
test hash_set::with_one_new_1000 ... bench: 59,051 ns/iter (+/- 1,202)
test hash_set::with_one_new_rc_str_1000_baseline ... bench: 37,986 ns/iter (+/- 771)
2019-12-10 15:32:25 -05:00
|
|
|
mod fx {
|
2019-12-06 15:03:29 -05:00
|
|
|
use super::*;
|
tamer::sym: FNV => Fx Hash
For strings of any notable length, Fx Hash outperforms FNV. Rustc also
moved to this hash function and noticed performance
improvements. Fortunately, as was accounted for in the design, this was a
trivial switch.
Here are some benchmarks to back up that claim:
test hash_set::fnv::with_all_new_1000 ... bench: 133,096 ns/iter (+/- 1,430)
test hash_set::fnv::with_all_new_1000_with_capacity ... bench: 82,591 ns/iter (+/- 592)
test hash_set::fnv::with_all_new_rc_str_1000_baseline ... bench: 162,073 ns/iter (+/- 1,277)
test hash_set::fnv::with_one_new_1000 ... bench: 37,334 ns/iter (+/- 256)
test hash_set::fnv::with_one_new_rc_str_1000_baseline ... bench: 18,263 ns/iter (+/- 261)
test hash_set::fx::with_all_new_1000 ... bench: 85,217 ns/iter (+/- 1,111)
test hash_set::fx::with_all_new_1000_with_capacity ... bench: 59,383 ns/iter (+/- 752)
test hash_set::fx::with_all_new_rc_str_1000_baseline ... bench: 98,802 ns/iter (+/- 1,117)
test hash_set::fx::with_one_new_1000 ... bench: 42,484 ns/iter (+/- 1,239)
test hash_set::fx::with_one_new_rc_str_1000_baseline ... bench: 15,000 ns/iter (+/- 233)
test hash_set::with_all_new_1000 ... bench: 137,645 ns/iter (+/- 1,186)
test hash_set::with_all_new_rc_str_1000_baseline ... bench: 163,129 ns/iter (+/- 1,725)
test hash_set::with_one_new_1000 ... bench: 59,051 ns/iter (+/- 1,202)
test hash_set::with_one_new_rc_str_1000_baseline ... bench: 37,986 ns/iter (+/- 771)
2019-12-10 15:32:25 -05:00
|
|
|
use fxhash::FxBuildHasher;
|
2019-12-06 15:03:29 -05:00
|
|
|
|
|
|
|
/// This is our baseline with a raw Rc<str>.
|
|
|
|
#[bench]
|
|
|
|
fn with_all_new_rc_str_1000_baseline(bench: &mut Bencher) {
|
|
|
|
let strs = gen_strs(1000);
|
|
|
|
bench.iter(|| {
|
tamer::sym: FNV => Fx Hash
For strings of any notable length, Fx Hash outperforms FNV. Rustc also
moved to this hash function and noticed performance
improvements. Fortunately, as was accounted for in the design, this was a
trivial switch.
Here are some benchmarks to back up that claim:
test hash_set::fnv::with_all_new_1000 ... bench: 133,096 ns/iter (+/- 1,430)
test hash_set::fnv::with_all_new_1000_with_capacity ... bench: 82,591 ns/iter (+/- 592)
test hash_set::fnv::with_all_new_rc_str_1000_baseline ... bench: 162,073 ns/iter (+/- 1,277)
test hash_set::fnv::with_one_new_1000 ... bench: 37,334 ns/iter (+/- 256)
test hash_set::fnv::with_one_new_rc_str_1000_baseline ... bench: 18,263 ns/iter (+/- 261)
test hash_set::fx::with_all_new_1000 ... bench: 85,217 ns/iter (+/- 1,111)
test hash_set::fx::with_all_new_1000_with_capacity ... bench: 59,383 ns/iter (+/- 752)
test hash_set::fx::with_all_new_rc_str_1000_baseline ... bench: 98,802 ns/iter (+/- 1,117)
test hash_set::fx::with_one_new_1000 ... bench: 42,484 ns/iter (+/- 1,239)
test hash_set::fx::with_one_new_rc_str_1000_baseline ... bench: 15,000 ns/iter (+/- 233)
test hash_set::with_all_new_1000 ... bench: 137,645 ns/iter (+/- 1,186)
test hash_set::with_all_new_rc_str_1000_baseline ... bench: 163,129 ns/iter (+/- 1,725)
test hash_set::with_one_new_1000 ... bench: 59,051 ns/iter (+/- 1,202)
test hash_set::with_one_new_rc_str_1000_baseline ... bench: 37,986 ns/iter (+/- 771)
2019-12-10 15:32:25 -05:00
|
|
|
let mut sut = HashSetSut::<FxBuildHasher>::new();
|
2019-12-06 15:03:29 -05:00
|
|
|
strs.iter().map(|s| sut.intern(&s)).for_each(drop);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2021-08-13 22:54:04 -04:00
|
|
|
// For comparison with uninterned symbols.
|
|
|
|
#[bench]
|
|
|
|
fn with_all_new_owned_string_1000_baseline(bench: &mut Bencher) {
|
|
|
|
let strs = gen_strs(1000);
|
|
|
|
|
|
|
|
bench.iter(|| {
|
tamer: tameld: Use uninterned symbols for reader
Fragments were previously represented by `String` to avoid the cost of
interning (hashing and copying). This change modifies it to use uninterned
symbols, which does still have a copy overhead but it does not hash.
Initial tests shows a small performance decrease of about 15% and a small
memory increase of similar proportion. However, once I realized that I was
not clearing buffers from quick_xml events and implemented that change in a
previous commit, this change ended up being approximately on par with
`String`, despite the copying of some pretty large fragments.
YMMV, though, and perhaps on less powerful systems time may increase
slightly.
The upcoming XIR (XML IR) was originally going to support both owned strings
and symbols, but now we'll just use uninterned symbols; I can't rationalize
complicating the API at this time when it will provide an almost
imperceivable performance benefit. If ever that changes in the future,
that change will be entertained.
The end result is that the fate of a fragment's underlying memory is
determined by whatever is processing the data, _not_ by the API itself---the
API was previously forcing use of a String, whereas now it's up to the
caller to determine whether we want comparable interns. For fragments,
that's not likely ever to be the case, especially considering that the
representation will change so drastically in the future.
2021-08-16 14:01:20 -04:00
|
|
|
let _sut = ArenaInterner::<FxBuildHasher, u32>::new();
|
2021-08-13 22:54:04 -04:00
|
|
|
strs.iter().map(|s| String::from(s)).for_each(drop);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2019-12-06 15:03:29 -05:00
|
|
|
#[bench]
|
|
|
|
fn with_all_new_1000(bench: &mut Bencher) {
|
|
|
|
let strs = gen_strs(1000);
|
|
|
|
|
|
|
|
bench.iter(|| {
|
2021-08-02 23:54:37 -04:00
|
|
|
let sut = ArenaInterner::<FxBuildHasher, u32>::new();
|
2019-12-06 15:03:29 -05:00
|
|
|
strs.iter().map(|s| sut.intern(&s)).for_each(drop);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2021-10-18 21:32:21 -04:00
|
|
|
// Unlike the above, which already has a UTF-8 string, this actually
|
|
|
|
// performs a conversion and check from `&[u8]`.
|
|
|
|
#[bench]
|
|
|
|
fn with_all_new_1000_utf8_checked(bench: &mut Bencher) {
|
|
|
|
let strs = gen_strs(1000);
|
|
|
|
let bs: Vec<&[u8]> = strs.iter().map(|s| s.as_bytes()).collect();
|
|
|
|
|
|
|
|
bench.iter(|| {
|
|
|
|
let sut = ArenaInterner::<FxBuildHasher, u32>::new();
|
|
|
|
bs.iter()
|
|
|
|
.map(|b| sut.intern(std::str::from_utf8(&b).unwrap()))
|
|
|
|
.for_each(drop);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2021-10-19 13:41:54 -04:00
|
|
|
// Using `Interner::intern_utf8`.
|
|
|
|
#[bench]
|
|
|
|
fn with_all_new_1000_intern_utf8(bench: &mut Bencher) {
|
|
|
|
let strs = gen_strs(1000);
|
|
|
|
let bs: Vec<&[u8]> = strs.iter().map(|s| s.as_bytes()).collect();
|
|
|
|
|
|
|
|
bench.iter(|| {
|
|
|
|
let sut = ArenaInterner::<FxBuildHasher, u32>::new();
|
|
|
|
bs.iter()
|
|
|
|
.map(|b| sut.intern_utf8(&b).unwrap())
|
|
|
|
.for_each(drop);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2021-10-18 21:32:21 -04:00
|
|
|
#[bench]
|
|
|
|
fn with_all_new_1000_utf8_unchecked(bench: &mut Bencher) {
|
|
|
|
let strs = gen_strs(1000);
|
|
|
|
let bs: Vec<&[u8]> = strs.iter().map(|s| s.as_bytes()).collect();
|
|
|
|
|
|
|
|
bench.iter(|| {
|
|
|
|
let sut = ArenaInterner::<FxBuildHasher, u32>::new();
|
|
|
|
bs.iter()
|
|
|
|
.map(|b| unsafe { sut.intern_utf8_unchecked(&b) })
|
|
|
|
.for_each(drop);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2021-08-13 22:54:04 -04:00
|
|
|
#[bench]
|
|
|
|
fn with_all_new_uninterned_1000(bench: &mut Bencher) {
|
|
|
|
let strs = gen_strs(1000);
|
|
|
|
|
|
|
|
bench.iter(|| {
|
|
|
|
let sut = ArenaInterner::<FxBuildHasher, u32>::new();
|
|
|
|
strs.iter().map(|s| sut.clone_uninterned(&s)).for_each(drop);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2019-12-06 15:03:29 -05:00
|
|
|
#[bench]
|
|
|
|
/// This is our baseline with a raw Rc<str>.
|
|
|
|
fn with_one_new_rc_str_1000_baseline(bench: &mut Bencher) {
|
|
|
|
bench.iter(|| {
|
tamer::sym: FNV => Fx Hash
For strings of any notable length, Fx Hash outperforms FNV. Rustc also
moved to this hash function and noticed performance
improvements. Fortunately, as was accounted for in the design, this was a
trivial switch.
Here are some benchmarks to back up that claim:
test hash_set::fnv::with_all_new_1000 ... bench: 133,096 ns/iter (+/- 1,430)
test hash_set::fnv::with_all_new_1000_with_capacity ... bench: 82,591 ns/iter (+/- 592)
test hash_set::fnv::with_all_new_rc_str_1000_baseline ... bench: 162,073 ns/iter (+/- 1,277)
test hash_set::fnv::with_one_new_1000 ... bench: 37,334 ns/iter (+/- 256)
test hash_set::fnv::with_one_new_rc_str_1000_baseline ... bench: 18,263 ns/iter (+/- 261)
test hash_set::fx::with_all_new_1000 ... bench: 85,217 ns/iter (+/- 1,111)
test hash_set::fx::with_all_new_1000_with_capacity ... bench: 59,383 ns/iter (+/- 752)
test hash_set::fx::with_all_new_rc_str_1000_baseline ... bench: 98,802 ns/iter (+/- 1,117)
test hash_set::fx::with_one_new_1000 ... bench: 42,484 ns/iter (+/- 1,239)
test hash_set::fx::with_one_new_rc_str_1000_baseline ... bench: 15,000 ns/iter (+/- 233)
test hash_set::with_all_new_1000 ... bench: 137,645 ns/iter (+/- 1,186)
test hash_set::with_all_new_rc_str_1000_baseline ... bench: 163,129 ns/iter (+/- 1,725)
test hash_set::with_one_new_1000 ... bench: 59,051 ns/iter (+/- 1,202)
test hash_set::with_one_new_rc_str_1000_baseline ... bench: 37,986 ns/iter (+/- 771)
2019-12-10 15:32:25 -05:00
|
|
|
let mut sut: HashSetSut<FxBuildHasher> = HashSetSut {
|
2019-12-06 15:03:29 -05:00
|
|
|
map: HashSet::with_hasher(Default::default()),
|
|
|
|
};
|
|
|
|
(0..1000).map(|_| sut.intern("first")).for_each(drop);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
#[bench]
|
|
|
|
fn with_one_new_1000(bench: &mut Bencher) {
|
|
|
|
bench.iter(|| {
|
2021-08-02 23:54:37 -04:00
|
|
|
let sut = ArenaInterner::<FxBuildHasher, u32>::new();
|
2019-12-06 15:03:29 -05:00
|
|
|
(0..1000).map(|_| sut.intern("first")).for_each(drop);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2021-10-18 21:32:21 -04:00
|
|
|
// Unlike the above, which already has a UTF-8 string, this actually
|
|
|
|
// performs a conversion and check from `&[u8]`.
|
|
|
|
#[bench]
|
|
|
|
fn with_one_new_1000_utf8_checked(bench: &mut Bencher) {
|
|
|
|
bench.iter(|| {
|
|
|
|
let sut = ArenaInterner::<FxBuildHasher, u32>::new();
|
|
|
|
(0..1000)
|
|
|
|
.map(|_| sut.intern(std::str::from_utf8(b"first").unwrap()))
|
|
|
|
.for_each(drop);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2021-10-19 13:41:54 -04:00
|
|
|
// Using `Interner::intern_utf8`.
|
|
|
|
#[bench]
|
|
|
|
fn with_one_new_1000_intern_utf8(bench: &mut Bencher) {
|
|
|
|
bench.iter(|| {
|
|
|
|
let sut = ArenaInterner::<FxBuildHasher, u32>::new();
|
|
|
|
(0..1000)
|
|
|
|
.map(|_| sut.intern_utf8(b"first").unwrap())
|
|
|
|
.for_each(drop);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2020-01-24 11:09:24 -05:00
|
|
|
#[bench]
|
|
|
|
fn with_one_new_1000_utf8_unchecked(bench: &mut Bencher) {
|
|
|
|
bench.iter(|| {
|
2021-08-02 23:54:37 -04:00
|
|
|
let sut = ArenaInterner::<FxBuildHasher, u32>::new();
|
2020-01-24 11:09:24 -05:00
|
|
|
(0..1000)
|
|
|
|
.map(|_| unsafe { sut.intern_utf8_unchecked(b"first") })
|
|
|
|
.for_each(drop);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
tamer::sym: FNV => Fx Hash
For strings of any notable length, Fx Hash outperforms FNV. Rustc also
moved to this hash function and noticed performance
improvements. Fortunately, as was accounted for in the design, this was a
trivial switch.
Here are some benchmarks to back up that claim:
test hash_set::fnv::with_all_new_1000 ... bench: 133,096 ns/iter (+/- 1,430)
test hash_set::fnv::with_all_new_1000_with_capacity ... bench: 82,591 ns/iter (+/- 592)
test hash_set::fnv::with_all_new_rc_str_1000_baseline ... bench: 162,073 ns/iter (+/- 1,277)
test hash_set::fnv::with_one_new_1000 ... bench: 37,334 ns/iter (+/- 256)
test hash_set::fnv::with_one_new_rc_str_1000_baseline ... bench: 18,263 ns/iter (+/- 261)
test hash_set::fx::with_all_new_1000 ... bench: 85,217 ns/iter (+/- 1,111)
test hash_set::fx::with_all_new_1000_with_capacity ... bench: 59,383 ns/iter (+/- 752)
test hash_set::fx::with_all_new_rc_str_1000_baseline ... bench: 98,802 ns/iter (+/- 1,117)
test hash_set::fx::with_one_new_1000 ... bench: 42,484 ns/iter (+/- 1,239)
test hash_set::fx::with_one_new_rc_str_1000_baseline ... bench: 15,000 ns/iter (+/- 233)
test hash_set::with_all_new_1000 ... bench: 137,645 ns/iter (+/- 1,186)
test hash_set::with_all_new_rc_str_1000_baseline ... bench: 163,129 ns/iter (+/- 1,725)
test hash_set::with_one_new_1000 ... bench: 59,051 ns/iter (+/- 1,202)
test hash_set::with_one_new_rc_str_1000_baseline ... bench: 37,986 ns/iter (+/- 771)
2019-12-10 15:32:25 -05:00
|
|
|
/// Since Fx is the best-performing, let's build upon it to demonstrate
|
2019-12-06 15:03:29 -05:00
|
|
|
/// the benefits of with_capacity
|
|
|
|
#[bench]
|
|
|
|
fn with_all_new_1000_with_capacity(bench: &mut Bencher) {
|
|
|
|
let n = 1000;
|
|
|
|
let strs = gen_strs(n);
|
|
|
|
|
|
|
|
bench.iter(|| {
|
2021-08-02 23:54:37 -04:00
|
|
|
let sut = ArenaInterner::<FxBuildHasher, u32>::with_capacity(n);
|
2019-12-06 15:03:29 -05:00
|
|
|
strs.iter().map(|s| sut.intern(&s)).for_each(drop);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
tamer: Global interners
This is a major change, and I apologize for it all being in one commit. I
had wanted to break it up, but doing so would have required a significant
amount of temporary work that was not worth doing while I'm the only one
working on this project at the moment.
This accomplishes a number of important things, now that I'm preparing to
write the first compiler frontend for TAMER:
1. `Symbol` has been removed; `SymbolId` is used in its place.
2. Consequently, symbols use 16 or 32 bits, rather than a 64-bit pointer.
3. Using symbols no longer requires dereferencing.
4. **Lifetimes no longer pollute the entire system! (`'i`)**
5. Two global interners are offered to produce `SymbolStr` with `'static`
lifetimes, simplfiying lifetime management and borrowing where strings
are still needed.
6. A nice API is provided for interning and lookups (e.g. "foo".intern())
which makes this look like a core feature of Rust.
Unfortunately, making this change required modifications to...virtually
everything. And that serves to emphasize why this change was needed:
_everything_ used symbols, and so there's no use in not providing globals.
I implemented this in a way that still provides for loose coupling through
Rust's trait system. Indeed, Rustc offers a global interner, and I decided
not to go that route initially because it wasn't clear to me that such a
thing was desirable. It didn't become apparent to me, in fact, until the
recent commit where I introduced `SymbolIndexSize` and saw how many things
had to be touched; the linker evolved so rapidly as I was trying to learn
Rust that I lost track of how bad it got.
Further, this shows how the design of the internment system was a bit
naive---I assumed certain requirements that never panned out. In
particular, everything using symbols stored `&'i Symbol<'i>`---that is, a
reference (usize) to an object containing an index (32-bit) and a string
slice (128-bit). So it was a reference to a pretty large value, which was
allocated in the arena alongside the interned string itself.
But, that was assuming that something would need both the symbol index _and_
a readily available string. That's not the case. In fact, it's pretty
clear that interning happens at the beginning of execution, that `SymbolId`
is all that's needed during processing (unless an error occurs; more on that
below); and it's not until _the very end_ that we need to retrieve interned
strings from the pool to write either to a file or to display to the
user. It was horribly wasteful!
So `SymbolId` solves the lifetime issue in itself for most systems, but it
still requires that an interner be available for anything that needs to
create or resolve symbols, which, as it turns out, is still a lot of
things. Therefore, I decided to implement them as thread-local static
variables, which is very similar to what Rustc does itself (Rustc's are
scoped). TAMER does not use threads, so the resulting `'static` lifetime
should be just fine for now. Eventually I'd like to implement `!Send` and
`!Sync`, though, to prevent references from escaping the thread (as noted in
the patch); I can't do that yet, since the feature has not yet been
stabalized.
In the end, this leaves us with a system that's much easier to use and
maintain; hopefully easier for newcomers to get into without having to deal
with so many complex lifetimes; and a nice API that makes it a pleasure to
work with symbols.
Admittedly, the `SymbolIndexSize` adds some complexity, and we'll see if I
end up regretting that down the line, but it exists for an important reason:
the `Span` and other structures that'll be introduced need to pack a lot of
data into 64 bits so they can be freely copied around to keep lifetimes
simple without wreaking havoc in other ways, but a 32-bit symbol size needed
by the linker is too large for that. (Actually, the linker doesn't yet need
32 bits for our systems, but it's going to in the somewhat near future
unless we optimize away a bunch of symbols...but I'd really rather not have
the linker hit a limit that requires a lot of code changes to resolve).
Rustc uses interned spans when they exceed 8 bytes, but I'd prefer to avoid
that for now. Most systems can just use on of the `PkgSymbolId` or
`ProgSymbolId` type aliases and not have to worry about it. Systems that
are actually shared between the compiler and the linker do, though, but it's
not like we don't already have a bunch of trait bounds.
Of course, as we implement link-time optimizations (LTO) in the future, it's
possible most things will need the size and I'll grow frustrated with that
and possibly revisit this. We shall see.
Anyway, this was exhausting...and...onward to the first frontend!
2021-08-02 23:54:37 -04:00
|
|
|
|
|
|
|
// Note that these tests don't drop the global interner in-between.
|
|
|
|
mod global {
|
|
|
|
use super::*;
|
|
|
|
use tamer::sym::GlobalSymbolIntern;
|
|
|
|
|
|
|
|
#[bench]
|
|
|
|
fn with_all_new_1000(bench: &mut Bencher) {
|
|
|
|
let strs = gen_strs(1000);
|
|
|
|
|
|
|
|
bench.iter(|| {
|
|
|
|
strs.iter()
|
2021-09-23 14:52:53 -04:00
|
|
|
.map::<SymbolId, _>(|s| s.intern())
|
tamer: Global interners
This is a major change, and I apologize for it all being in one commit. I
had wanted to break it up, but doing so would have required a significant
amount of temporary work that was not worth doing while I'm the only one
working on this project at the moment.
This accomplishes a number of important things, now that I'm preparing to
write the first compiler frontend for TAMER:
1. `Symbol` has been removed; `SymbolId` is used in its place.
2. Consequently, symbols use 16 or 32 bits, rather than a 64-bit pointer.
3. Using symbols no longer requires dereferencing.
4. **Lifetimes no longer pollute the entire system! (`'i`)**
5. Two global interners are offered to produce `SymbolStr` with `'static`
lifetimes, simplfiying lifetime management and borrowing where strings
are still needed.
6. A nice API is provided for interning and lookups (e.g. "foo".intern())
which makes this look like a core feature of Rust.
Unfortunately, making this change required modifications to...virtually
everything. And that serves to emphasize why this change was needed:
_everything_ used symbols, and so there's no use in not providing globals.
I implemented this in a way that still provides for loose coupling through
Rust's trait system. Indeed, Rustc offers a global interner, and I decided
not to go that route initially because it wasn't clear to me that such a
thing was desirable. It didn't become apparent to me, in fact, until the
recent commit where I introduced `SymbolIndexSize` and saw how many things
had to be touched; the linker evolved so rapidly as I was trying to learn
Rust that I lost track of how bad it got.
Further, this shows how the design of the internment system was a bit
naive---I assumed certain requirements that never panned out. In
particular, everything using symbols stored `&'i Symbol<'i>`---that is, a
reference (usize) to an object containing an index (32-bit) and a string
slice (128-bit). So it was a reference to a pretty large value, which was
allocated in the arena alongside the interned string itself.
But, that was assuming that something would need both the symbol index _and_
a readily available string. That's not the case. In fact, it's pretty
clear that interning happens at the beginning of execution, that `SymbolId`
is all that's needed during processing (unless an error occurs; more on that
below); and it's not until _the very end_ that we need to retrieve interned
strings from the pool to write either to a file or to display to the
user. It was horribly wasteful!
So `SymbolId` solves the lifetime issue in itself for most systems, but it
still requires that an interner be available for anything that needs to
create or resolve symbols, which, as it turns out, is still a lot of
things. Therefore, I decided to implement them as thread-local static
variables, which is very similar to what Rustc does itself (Rustc's are
scoped). TAMER does not use threads, so the resulting `'static` lifetime
should be just fine for now. Eventually I'd like to implement `!Send` and
`!Sync`, though, to prevent references from escaping the thread (as noted in
the patch); I can't do that yet, since the feature has not yet been
stabalized.
In the end, this leaves us with a system that's much easier to use and
maintain; hopefully easier for newcomers to get into without having to deal
with so many complex lifetimes; and a nice API that makes it a pleasure to
work with symbols.
Admittedly, the `SymbolIndexSize` adds some complexity, and we'll see if I
end up regretting that down the line, but it exists for an important reason:
the `Span` and other structures that'll be introduced need to pack a lot of
data into 64 bits so they can be freely copied around to keep lifetimes
simple without wreaking havoc in other ways, but a 32-bit symbol size needed
by the linker is too large for that. (Actually, the linker doesn't yet need
32 bits for our systems, but it's going to in the somewhat near future
unless we optimize away a bunch of symbols...but I'd really rather not have
the linker hit a limit that requires a lot of code changes to resolve).
Rustc uses interned spans when they exceed 8 bytes, but I'd prefer to avoid
that for now. Most systems can just use on of the `PkgSymbolId` or
`ProgSymbolId` type aliases and not have to worry about it. Systems that
are actually shared between the compiler and the linker do, though, but it's
not like we don't already have a bunch of trait bounds.
Of course, as we implement link-time optimizations (LTO) in the future, it's
possible most things will need the size and I'll grow frustrated with that
and possibly revisit this. We shall see.
Anyway, this was exhausting...and...onward to the first frontend!
2021-08-02 23:54:37 -04:00
|
|
|
.for_each(drop);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
#[bench]
|
|
|
|
fn with_one_new_1000(bench: &mut Bencher) {
|
|
|
|
bench.iter(|| {
|
|
|
|
(0..1000)
|
2021-09-23 14:52:53 -04:00
|
|
|
.map::<SymbolId, _>(|_| "onenew".intern())
|
tamer: Global interners
This is a major change, and I apologize for it all being in one commit. I
had wanted to break it up, but doing so would have required a significant
amount of temporary work that was not worth doing while I'm the only one
working on this project at the moment.
This accomplishes a number of important things, now that I'm preparing to
write the first compiler frontend for TAMER:
1. `Symbol` has been removed; `SymbolId` is used in its place.
2. Consequently, symbols use 16 or 32 bits, rather than a 64-bit pointer.
3. Using symbols no longer requires dereferencing.
4. **Lifetimes no longer pollute the entire system! (`'i`)**
5. Two global interners are offered to produce `SymbolStr` with `'static`
lifetimes, simplfiying lifetime management and borrowing where strings
are still needed.
6. A nice API is provided for interning and lookups (e.g. "foo".intern())
which makes this look like a core feature of Rust.
Unfortunately, making this change required modifications to...virtually
everything. And that serves to emphasize why this change was needed:
_everything_ used symbols, and so there's no use in not providing globals.
I implemented this in a way that still provides for loose coupling through
Rust's trait system. Indeed, Rustc offers a global interner, and I decided
not to go that route initially because it wasn't clear to me that such a
thing was desirable. It didn't become apparent to me, in fact, until the
recent commit where I introduced `SymbolIndexSize` and saw how many things
had to be touched; the linker evolved so rapidly as I was trying to learn
Rust that I lost track of how bad it got.
Further, this shows how the design of the internment system was a bit
naive---I assumed certain requirements that never panned out. In
particular, everything using symbols stored `&'i Symbol<'i>`---that is, a
reference (usize) to an object containing an index (32-bit) and a string
slice (128-bit). So it was a reference to a pretty large value, which was
allocated in the arena alongside the interned string itself.
But, that was assuming that something would need both the symbol index _and_
a readily available string. That's not the case. In fact, it's pretty
clear that interning happens at the beginning of execution, that `SymbolId`
is all that's needed during processing (unless an error occurs; more on that
below); and it's not until _the very end_ that we need to retrieve interned
strings from the pool to write either to a file or to display to the
user. It was horribly wasteful!
So `SymbolId` solves the lifetime issue in itself for most systems, but it
still requires that an interner be available for anything that needs to
create or resolve symbols, which, as it turns out, is still a lot of
things. Therefore, I decided to implement them as thread-local static
variables, which is very similar to what Rustc does itself (Rustc's are
scoped). TAMER does not use threads, so the resulting `'static` lifetime
should be just fine for now. Eventually I'd like to implement `!Send` and
`!Sync`, though, to prevent references from escaping the thread (as noted in
the patch); I can't do that yet, since the feature has not yet been
stabalized.
In the end, this leaves us with a system that's much easier to use and
maintain; hopefully easier for newcomers to get into without having to deal
with so many complex lifetimes; and a nice API that makes it a pleasure to
work with symbols.
Admittedly, the `SymbolIndexSize` adds some complexity, and we'll see if I
end up regretting that down the line, but it exists for an important reason:
the `Span` and other structures that'll be introduced need to pack a lot of
data into 64 bits so they can be freely copied around to keep lifetimes
simple without wreaking havoc in other ways, but a 32-bit symbol size needed
by the linker is too large for that. (Actually, the linker doesn't yet need
32 bits for our systems, but it's going to in the somewhat near future
unless we optimize away a bunch of symbols...but I'd really rather not have
the linker hit a limit that requires a lot of code changes to resolve).
Rustc uses interned spans when they exceed 8 bytes, but I'd prefer to avoid
that for now. Most systems can just use on of the `PkgSymbolId` or
`ProgSymbolId` type aliases and not have to worry about it. Systems that
are actually shared between the compiler and the linker do, though, but it's
not like we don't already have a bunch of trait bounds.
Of course, as we implement link-time optimizations (LTO) in the future, it's
possible most things will need the size and I'll grow frustrated with that
and possibly revisit this. We shall see.
Anyway, this was exhausting...and...onward to the first frontend!
2021-08-02 23:54:37 -04:00
|
|
|
.for_each(drop);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
#[bench]
|
|
|
|
fn with_one_new_1000_utf8_unchecked(bench: &mut Bencher) {
|
|
|
|
bench.iter(|| {
|
|
|
|
(0..1000)
|
2021-09-23 14:52:53 -04:00
|
|
|
.map::<SymbolId, _>(|_| unsafe {
|
tamer: Global interners
This is a major change, and I apologize for it all being in one commit. I
had wanted to break it up, but doing so would have required a significant
amount of temporary work that was not worth doing while I'm the only one
working on this project at the moment.
This accomplishes a number of important things, now that I'm preparing to
write the first compiler frontend for TAMER:
1. `Symbol` has been removed; `SymbolId` is used in its place.
2. Consequently, symbols use 16 or 32 bits, rather than a 64-bit pointer.
3. Using symbols no longer requires dereferencing.
4. **Lifetimes no longer pollute the entire system! (`'i`)**
5. Two global interners are offered to produce `SymbolStr` with `'static`
lifetimes, simplfiying lifetime management and borrowing where strings
are still needed.
6. A nice API is provided for interning and lookups (e.g. "foo".intern())
which makes this look like a core feature of Rust.
Unfortunately, making this change required modifications to...virtually
everything. And that serves to emphasize why this change was needed:
_everything_ used symbols, and so there's no use in not providing globals.
I implemented this in a way that still provides for loose coupling through
Rust's trait system. Indeed, Rustc offers a global interner, and I decided
not to go that route initially because it wasn't clear to me that such a
thing was desirable. It didn't become apparent to me, in fact, until the
recent commit where I introduced `SymbolIndexSize` and saw how many things
had to be touched; the linker evolved so rapidly as I was trying to learn
Rust that I lost track of how bad it got.
Further, this shows how the design of the internment system was a bit
naive---I assumed certain requirements that never panned out. In
particular, everything using symbols stored `&'i Symbol<'i>`---that is, a
reference (usize) to an object containing an index (32-bit) and a string
slice (128-bit). So it was a reference to a pretty large value, which was
allocated in the arena alongside the interned string itself.
But, that was assuming that something would need both the symbol index _and_
a readily available string. That's not the case. In fact, it's pretty
clear that interning happens at the beginning of execution, that `SymbolId`
is all that's needed during processing (unless an error occurs; more on that
below); and it's not until _the very end_ that we need to retrieve interned
strings from the pool to write either to a file or to display to the
user. It was horribly wasteful!
So `SymbolId` solves the lifetime issue in itself for most systems, but it
still requires that an interner be available for anything that needs to
create or resolve symbols, which, as it turns out, is still a lot of
things. Therefore, I decided to implement them as thread-local static
variables, which is very similar to what Rustc does itself (Rustc's are
scoped). TAMER does not use threads, so the resulting `'static` lifetime
should be just fine for now. Eventually I'd like to implement `!Send` and
`!Sync`, though, to prevent references from escaping the thread (as noted in
the patch); I can't do that yet, since the feature has not yet been
stabalized.
In the end, this leaves us with a system that's much easier to use and
maintain; hopefully easier for newcomers to get into without having to deal
with so many complex lifetimes; and a nice API that makes it a pleasure to
work with symbols.
Admittedly, the `SymbolIndexSize` adds some complexity, and we'll see if I
end up regretting that down the line, but it exists for an important reason:
the `Span` and other structures that'll be introduced need to pack a lot of
data into 64 bits so they can be freely copied around to keep lifetimes
simple without wreaking havoc in other ways, but a 32-bit symbol size needed
by the linker is too large for that. (Actually, the linker doesn't yet need
32 bits for our systems, but it's going to in the somewhat near future
unless we optimize away a bunch of symbols...but I'd really rather not have
the linker hit a limit that requires a lot of code changes to resolve).
Rustc uses interned spans when they exceed 8 bytes, but I'd prefer to avoid
that for now. Most systems can just use on of the `PkgSymbolId` or
`ProgSymbolId` type aliases and not have to worry about it. Systems that
are actually shared between the compiler and the linker do, though, but it's
not like we don't already have a bunch of trait bounds.
Of course, as we implement link-time optimizations (LTO) in the future, it's
possible most things will need the size and I'll grow frustrated with that
and possibly revisit this. We shall see.
Anyway, this was exhausting...and...onward to the first frontend!
2021-08-02 23:54:37 -04:00
|
|
|
(b"onenewu8").intern_utf8_unchecked()
|
|
|
|
})
|
|
|
|
.for_each(drop);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
2019-12-06 15:03:29 -05:00
|
|
|
}
|