Skip to content

A fast, cheap, compile-time constructible, Copy-able, kinda primitive inline string type. ๐˜ž๐˜ฉ๐˜ฆ๐˜ฏ ๐˜ด๐˜ต๐˜ฐ๐˜ณ๐˜ช๐˜ฏ๐˜จ ๐˜ต๐˜ฉ๐˜ฆ๐˜ด๐˜ฆ ๐˜ฐ๐˜ฏ ๐˜ต๐˜ฉ๐˜ฆ ๐˜ด๐˜ต๐˜ข๐˜ค๐˜ฌ, ๐˜บ๐˜ฐ๐˜ถ ๐˜ฑ๐˜ณ๐˜ฐ๐˜ฃ๐˜ข๐˜ฃ๐˜ญ๐˜บ ๐˜ธ๐˜ข๐˜ฏ๐˜ต ๐˜ต๐˜ฐ ๐˜ถ๐˜ด๐˜ฆ ๐˜ด๐˜ฎ๐˜ข๐˜ญ๐˜ญ๐˜ฆ๐˜ณ ๐˜ด๐˜ช๐˜ป๐˜ฆ๐˜ด, ๐˜ฉ๐˜ฆ๐˜ฏ๐˜ค๐˜ฆ ๐˜ต๐˜ฉ๐˜ฆ ๐˜ฏ๐˜ข๐˜ฎ๐˜ฆ. No dependencies are planned, except for optional SerDe support, etc. The intention is to be no-std.

License

Notifications You must be signed in to change notification settings

daniel-pfeiffer/stringlet

Folders and files

NameName
Last commit message
Last commit date

Latest commit

ย 

History

10 Commits
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

๐Ÿงต Stringlet

A fast, cheap, compile-time constructible, Copy-able, kinda primitive inline string type. When storing these on the stack, you probably want to use smaller sizes, hence the name. No dependencies are planned, except for optional SerDe support, etc. The intention is to be no-std and no-alloc โ€“ which still requires feature-gating String interop.

This is an alpha release. Using it as str should mostly work. The design is prepared for String functionality, but that needs to be implemented.

In my casual benchmarking it beats all other string kinds and crates nicely to spectacularly on some tests. There are four flavors of mostly the same code. They differ in length handling, which shows only in some operations, like len(), as_ref(), and as_str():

  • Stringlet, stringlet!(โ€ฆ): This is fixed size, i.e. bounds for array access are compiled in, hence fast.

  • VarStringlet, stringlet!(var โ€ฆ), stringlet!(v โ€ฆ): This adds one byte for the length โ€“ still pretty fast. Speed differs for some content processing, where SIMD gives an advantage for multiples of some power of 2, e.g. VarStringlet<32>. While for copying the advantage can be at one less, e.g. VarStringlet<31>. Length must be 0..=255.

  • TrimStringlet, stringlet!(trim โ€ฆ), stringlet!(t โ€ฆ): This can optionally trim one last byte, useful for codes with minimal length variation like ISO 639. This is achieved by tagging an unused last byte with a UTF-8 niche. The length gets calculated branchlessly with very few ops.

  • SlimStringlet, stringlet!(slim โ€ฆ), stringlet!(s โ€ฆ): This uses the same UTF-8 niche, but fully: It projects the length into 6 bits of the last byte, when content is less than full size. Length must be 0..=64. Though it is done branchlessly, there are a few more ops for length calculation. Hence this is the slowest, albeit by a small margin. Any bit hackers, who know how to do with less ops, welcome on board!

N.B.: Variable size VarStringlet seems a competitor to fixedstr::str, arrayvec::ArrayString, and the semi-official heapless::String. They lack a heapless::Str, to match the faster fixed size Stringlet. That would be given by fixedstr::zstr but their equality checks are not optimized. I hope it can be independently confirmed (or debunked, if I mismeasured) that for tasks like == Self or == &str all variants in this crate seem by a factor faster than competitors.

# extern crate stringlet;
use stringlet::{Stringlet, VarStringlet, TrimStringlet, SlimStringlet, stringlet};

let a: VarStringlet<10> = "shorter".into(); // override default stringlet size of 16 and donโ€™t use all of it
let b = a;
println!("{a} == {b}? {}", a == b);      // No โ€œvalue borrowed here after moveโ€ error ๐Ÿ˜‡

let nothing = Stringlet::<0>::new();     // Empty and zero size
let nil = VarStringlet::<5>::new();      // Empty and size 5 โ€“ would be impossible for fixed size Stringlet
let nada = TrimStringlet::<1>::new();    // Empty and size 1 โ€“ biggest an empty TrimStringlet can be

let x = stringlet!("Hello Rust!");       // Stringlet<11>
let y = stringlet!(v 14: "Hello Rust!"); // abbreviated VarStringlet<14>, more than length
let z = stringlet!(slim: "Hello Rust!"); // SlimStringlet<11>
let ฮจ = stringlet!(v: ["abcd", "abc", "ab"]); // VarStringlet<4> for each
let ฯ‰ = stringlet!(["abc", "def", "ghj"]); // Stringlet<3> for each

const HELLO: Stringlet<11> = stringlet!("Hello Rust!"); // Input length must match type
const PET: [Stringlet<3>; 4] = stringlet!(["cat", "dog", "ham", "pig"]); // size of 1st element
const PETS: [VarStringlet<8>; 4] = stringlet!(_: ["cat", "dog", "hamster", "piglet"]); // _: derive type

But

error[E0599]: `SlimStringlet<99>` has excessive SIZE
  --> src/main.rs:99:16
   |
99 | let balloons = stringlet!(s 99: "Luftballons, auf ihremโ€ฆ");
   |                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ SIZE must be `0..=64`
   โ€ฆ
   = note: the following trait bounds were not satisfied:
           `stringlet::StringletBase<stringlet::Slim, 99>: stringlet::SlimConfig<99>`
           which is required by `stringlet::StringletBase<stringlet::Slim, 99>: ConfigBase<stringlet::Slim, 99>`
   = note: `SlimStringlet` cannot be longer than 64 bytes. Consider using `VarStringlet`!

This is not your classical short string optimization (SSO) in so far as there is no overflow into an alternate bigger storage. This is by design, as addressing two different data routes requires branching. On your hot path, branch misprediction can be costly. Crate stringlet tries hard to be maximally branchless. The few ifs and ||s refer to constants, so should be optimized away.

There is no Option or Result niche optimization yet. That should likewise be feasible for all stringlets with a stored length. I only need to understand how to tell the compiler?

VarStringlet and SlimStringlet are configured so they can only be instantiated with valid sizes. For normal use thatโ€™s all there is to it. However when forwarding generic arguments to them you too have to bound by VarConfig<SIZE> or SlimConfig<SIZE>. I wish I could just hide it all behind <const SIZE: usize<0..=64>>!

Todo

  • StringletError & stringlet::Result

  • Run Miri on various architectures. Whoโ€™s willing to support with exotic stuff?

  • Run cargo mutants

  • Implement mutability, +=, write!().

  • Document!

  • How to implement Cow / Borrow with String as owned type?

  • Or rather a Cow-like storage-constrained/limitless pair that will transparently switch on overflow.

  • Implement more traits.

  • format!() equivalent stringlet!(format โ€ฆ) or format_stringlet!()

  • Integrate into string-rosetta-rs

  • Implement for popular 3rd party crates.

  • Why does this not pick up the default SIZE of 16: let fail = Stringlet::new();

  • Is there a downside to Copy by default?

  • Whatโ€™s our minimal rust-version?

Platt (Low German) Semi-literal Translation
Op de Straat lรถpptโ€™n Jung mitโ€™n Tรผddelband
inโ€™ne anner Handโ€™n Bodderbrood mit Kees,
wenn he blots ni mit de Been inโ€™n Tรผddel keem
un dor liggt he ok all lang op de Nees
un he rasselt mitโ€™n Dassel opโ€™n Kantsteen
un he bitt sick ganz geheurig op de Tung,
as he opsteiht, seggt he: Hett ni weeh doon,
datโ€™sโ€™n Klacks fรถr soโ€™n Kieler Jung
Upon the street runs a boy with a Twiddle-String
in another hand a buttered bread with cheese,
if he only not with the legs into the twiddle came
and there lies he already long upon the nose
and he rattles with the noggin upon a kerbstone
and he bites himself greatly upon the tongue,
as he up stands, says he: Has not hurt,
thatโ€™s a trifle for such a boy from Kiel

Dedicated to my father, who taught me this iconic Northgerman โ–ถ String song. The many similarities nicely illustrate how English (Anglo-Saxon) comes from Northgermany. There are even more cognates that have somewhat shifted in usage: lรถppt: elopes (runs) โ€“ Jung: young (boy) โ€“ Been: bones (legs) โ€“ weeh doon: woe done (has hurt) โ€“ and maybe Kant: cant (tilt on edge)

About

A fast, cheap, compile-time constructible, Copy-able, kinda primitive inline string type. ๐˜ž๐˜ฉ๐˜ฆ๐˜ฏ ๐˜ด๐˜ต๐˜ฐ๐˜ณ๐˜ช๐˜ฏ๐˜จ ๐˜ต๐˜ฉ๐˜ฆ๐˜ด๐˜ฆ ๐˜ฐ๐˜ฏ ๐˜ต๐˜ฉ๐˜ฆ ๐˜ด๐˜ต๐˜ข๐˜ค๐˜ฌ, ๐˜บ๐˜ฐ๐˜ถ ๐˜ฑ๐˜ณ๐˜ฐ๐˜ฃ๐˜ข๐˜ฃ๐˜ญ๐˜บ ๐˜ธ๐˜ข๐˜ฏ๐˜ต ๐˜ต๐˜ฐ ๐˜ถ๐˜ด๐˜ฆ ๐˜ด๐˜ฎ๐˜ข๐˜ญ๐˜ญ๐˜ฆ๐˜ณ ๐˜ด๐˜ช๐˜ป๐˜ฆ๐˜ด, ๐˜ฉ๐˜ฆ๐˜ฏ๐˜ค๐˜ฆ ๐˜ต๐˜ฉ๐˜ฆ ๐˜ฏ๐˜ข๐˜ฎ๐˜ฆ. No dependencies are planned, except for optional SerDe support, etc. The intention is to be no-std.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages