Commit Graph

33 Commits

Author SHA1 Message Date
Mike Hommey 0923b90948 Avoid slices in entries of decomposition tables
For small arrays of data, slices are expensive:
- there's the obvious size of the array (`sizeof::<char>() * length`)
- there's the size of the slice itself (`sizeof::<(*const str, usize)>()`)
- there's the size of the relocation for the pointer in the slice. The
  worst case is on 64-bits ELF, where it is `3 * sizeof::<usize>()`(!).

Most entries in decomposition tables are 2 characters or less, so the
overhead for each of these tables is incredibly large.

To give an idea, a print "Hello, World" (fresh from cargo new)
executable built with `--release` on my machine has 17712 bytes of
.rela.dyn (relocations) and 9520 bytes of .data.rel.ro (relocatable
read-only data).

Adding unicode-normalization as a dependency and changing the code to
`println!("{}", String::from_iter("Hello, world!".nfc()));`,
those jump to, respectively, 156336 and 147968 bytes.

For comparison, with unicode-normalization 0.1.8 (last release before
the perfect hashes), they were 18168 and 9872 bytes. This is however
compensated by the .text (code) being larger (314607 with 0.1.8 vs.
234639 with 0.1.19); likewise for .rodata (non-relocatable read-only
data) (225979 with 0.1.8, vs. 82523 with 0.1.19).

This can be alleviated by replacing slices with indexes into a unique
slice per decomposition table, overall saving 228K (while barely adding
to code size (160 bytes)). This also makes the overall cost of
unicode-normalization lower than what it was in 0.1.8.

As far as performance is concerned, at least on my machine, it makes
virtually no difference on `cargo bench`:

on master:
running 22 tests
test bench_is_nfc_ascii                      ... bench:          13 ns/iter (+/- 0)
test bench_is_nfc_normalized                 ... bench:          23 ns/iter (+/- 0)
test bench_is_nfc_not_normalized             ... bench:         347 ns/iter (+/- 2)
test bench_is_nfc_stream_safe_ascii          ... bench:          13 ns/iter (+/- 0)
test bench_is_nfc_stream_safe_normalized     ... bench:          31 ns/iter (+/- 0)
test bench_is_nfc_stream_safe_not_normalized ... bench:         374 ns/iter (+/- 2)
test bench_is_nfd_ascii                      ... bench:           9 ns/iter (+/- 0)
test bench_is_nfd_normalized                 ... bench:          29 ns/iter (+/- 2)
test bench_is_nfd_not_normalized             ... bench:           9 ns/iter (+/- 0)
test bench_is_nfd_stream_safe_ascii          ... bench:          16 ns/iter (+/- 0)
test bench_is_nfd_stream_safe_normalized     ... bench:          40 ns/iter (+/- 0)
test bench_is_nfd_stream_safe_not_normalized ... bench:           9 ns/iter (+/- 0)
test bench_nfc_ascii                         ... bench:         525 ns/iter (+/- 1)
test bench_nfc_long                          ... bench:     186,528 ns/iter (+/- 1,613)
test bench_nfd_ascii                         ... bench:         283 ns/iter (+/- 30)
test bench_nfd_long                          ... bench:     120,183 ns/iter (+/- 4,510)
test bench_nfkc_ascii                        ... bench:         513 ns/iter (+/- 1)
test bench_nfkc_long                         ... bench:     192,922 ns/iter (+/- 1,673)
test bench_nfkd_ascii                        ... bench:         276 ns/iter (+/- 30)
test bench_nfkd_long                         ... bench:     137,163 ns/iter (+/- 2,159)
test bench_streamsafe_adversarial            ... bench:         323 ns/iter (+/- 5)
test bench_streamsafe_ascii                  ... bench:          25 ns/iter (+/- 0)

with patch applied:
running 22 tests
test bench_is_nfc_ascii                      ... bench:          13 ns/iter (+/- 0)
test bench_is_nfc_normalized                 ... bench:          23 ns/iter (+/- 0)
test bench_is_nfc_not_normalized             ... bench:         347 ns/iter (+/- 7)
test bench_is_nfc_stream_safe_ascii          ... bench:          13 ns/iter (+/- 0)
test bench_is_nfc_stream_safe_normalized     ... bench:          36 ns/iter (+/- 1)
test bench_is_nfc_stream_safe_not_normalized ... bench:         377 ns/iter (+/- 14)
test bench_is_nfd_ascii                      ... bench:           9 ns/iter (+/- 0)
test bench_is_nfd_normalized                 ... bench:          29 ns/iter (+/- 3)
test bench_is_nfd_not_normalized             ... bench:          10 ns/iter (+/- 0)
test bench_is_nfd_stream_safe_ascii          ... bench:          16 ns/iter (+/- 0)
test bench_is_nfd_stream_safe_normalized     ... bench:          39 ns/iter (+/- 1)
test bench_is_nfd_stream_safe_not_normalized ... bench:          10 ns/iter (+/- 0)
test bench_nfc_ascii                         ... bench:         545 ns/iter (+/- 2)
test bench_nfc_long                          ... bench:     186,348 ns/iter (+/- 1,660)
test bench_nfd_ascii                         ... bench:         281 ns/iter (+/- 2)
test bench_nfd_long                          ... bench:     124,720 ns/iter (+/- 5,967)
test bench_nfkc_ascii                        ... bench:         517 ns/iter (+/- 4)
test bench_nfkc_long                         ... bench:     194,943 ns/iter (+/- 1,636)
test bench_nfkd_ascii                        ... bench:         274 ns/iter (+/- 0)
test bench_nfkd_long                         ... bench:     127,973 ns/iter (+/- 1,161)
test bench_streamsafe_adversarial            ... bench:         320 ns/iter (+/- 3)
test bench_streamsafe_ascii                  ... bench:          25 ns/iter (+/- 0)
2022-06-23 18:10:18 +09:00
Dan Gohman 33e73008da Fix is_public_assigned to include Hangul Syllable and other ranges.
Hangul Syllables and several other ranges are defined in UnicodeData.txt
as just their first and last values:

```
AC00;<Hangul Syllable, First>;Lo;0;L;;;;;N;;;;;
D7A3;<Hangul Syllable, Last>;Lo;0;L;;;;;N;;;;;
```

Teach the unicode.py script how to recognize these, so that it correctly
classifies them as assigned ranges, for the `is_public_assigned`
predicate.
2021-06-01 14:42:29 -07:00
Dan Gohman aaa72a31da Add an is_public_assigned predicate.
Add an `is_public_assigned` predicate, which tests whether a given
`char` is assigned (`General_Category` != `Unassigned`) in the currently
supported version of Unicode, and not Private-Use (`General_Category`
!= `Private_Use`).

This comes up in some use cases sensitive to the stability of NFC over
Unicode version changes. An unassigned codepoint could become assigned in
the future, and new normalizations could apply to it.

For further details, see
 - <https://unicode.org/reports/tr15/#Versioning>
2021-05-28 09:13:34 -07:00
Dan Gohman ad609637a3 Fix the CJK Compat Variants decomp stats string.
Also, remove the non-`fully` `cjk_compat_variants_decomp` map, since
it's no longer used.
2021-01-06 10:01:25 -08:00
Dan Gohman 0083e1014e Rename svar to cjk_compat_variants. 2021-01-05 22:51:46 -08:00
Dan Gohman f362213cfb Switch to a more explicit API.
Switch to a dedicated `svar()` iterator function, which just does
standardized variation sequences, rather than framing this functionality
as an open-ended "extended" version of the standard normalization
algorithms. This makes for a more factored API, gives users more control
over exactly what transformations are done, and has less impact on users
that don't need this new functionality.
2020-12-06 08:26:50 -08:00
Dan Gohman 107879735b Add new normalization algorithms using Standardized Variants
The standard normalization algorithm decomposes CJK compatibility ideographs
into nominally equivalent codepoints, but which traditionally look different,
and is one of the main reasons normalization is considered destructive in
practice.

[Unicode 6.3] introduced a solution for this, by providing
[standardized variation sequences] for these codepoints. For example, while
U+2F8A6 "CJK COMPATIBILITY-IDEOGRAPH-2F8A6" canonically decomposes to U+6148
with a different appearance, in Unicode 6.3 and later the standardized variation
sequences in the StandardizedVariants.txt file include the following:

> 6148 FE00; CJK COMPATIBILITY IDEOGRAPH-2F8A6;

which says that "CJK COMPATIBILITY IDEOGRAPH-2F8A6" corresponds to
U+6148 U+FE00, where U+FE00 is "VARIATION SELECTOR-1".

U+6148 and U+FE00 are both normalized codepoints, so we can transform text
containing U+2F8A6 into normal form without losing information about the
distinct appearance. At this time, many popular implementations ignore these
variation selectors, however this technique at least preserves the information
in a standardized way, so implementations could use it if they chose.

This PR adds "ext" versions of the `nfd`, `nfc`, `nfkd`, and `nkfd`
iterators, which perform the standard algorithms extended with this technique.
They don't match the standard decompositions, and don't guarantee stability,
but they do produce appropriately normalized output.

I used the generic term "ext" to reflect that other extensions could
theoretically be added in the future. The standard decomposition tables are
limited by their stability requirements, but these "ext" versions could be
free to adopt new useful rules.

I'm not an expert in any of these topics, so please correct me if I'm mistaken
in any of this. Also, I'm open to ideas about how to best present this
functionality in the API.

[Unicode 6.3]: https://www.unicode.org/versions/Unicode6.3.0/#Summary
[standardized variation sequences]: http://unicode.org/faq/vs.html
2020-12-06 08:18:20 -08:00
Manish Goregaokar f01e1c882a Fix https url 2020-06-30 11:40:48 -07:00
Pyfisch bd1fcfbfab Update to Unicode 13
Change version fields to u8 as the maximum value
of each field is 255 according to specification.
2020-04-13 11:58:40 +02:00
Hannes Karppila 9e3b5169d6 Fix generated import statements in tables.rs 2020-04-10 23:35:11 +03:00
Manish Goregaokar a29396492b Add #[allow(ellipsis_inclusive_range_patterns)] to unicode.py 2019-11-22 08:30:05 -08:00
Raph Levien 4e56dbc9dd One more comment 2019-04-16 10:44:30 -07:00
Raph Levien 9aacd59532 Move code out of tables
The code has been moved out of the tables module into perfect_hash, and
there is a bit more explanation in comments.
2019-04-16 10:42:11 -07:00
Raph Levien 93d8afb900 Use perfect hashing for compose lookup 2019-04-09 17:51:03 -07:00
Raph Levien df37994166 Use perfect hashing for decomposition also 2019-04-09 17:20:27 -07:00
Raph Levien b68dbec11f More lookups moved to perfect hashing 2019-04-09 16:46:28 -07:00
Raph Levien 274b267525 Use minimal perfect hashing for combining class lookup 2019-04-09 12:26:24 -07:00
Raph Levien 6407e5f127 Update unicode script to Python 3 2019-04-09 10:43:25 -07:00
Sujay Jayakar e03d61cce8 Merge pull request #36 from djudd/compatibility-quick-checks
Add quick check implementations for NFKC & NFKD
2019-01-14 22:40:37 +05:30
David Judd 5685166427 Add quick check implementations for NFKC & NFKD 2019-01-12 14:44:53 -08:00
Björn Steinbrink 41914c5d30 Generate code that is friendlier to rustc's item_bodies_checking
Having the Some() constructor in each match arm body puts extras stress
on rustc's item_bodies_checking pass. We can work around that by moving
the Some() constructor around the match, and directly returning None
from the default arm, which is the only one that generates a None value.

On my box, this almost cuts the time spent in item_bodies_checking in
half, going from about 8.7s to about 4.6s, and reduces the complete
compile time from about 13s to about 9s, so about a third less.

There are no changes in performance in `cargo bench`.

Note that doing the same for the composition_table() function does not
yield any compile time wins, but would cause a performance regression.

cc #29
2019-01-08 17:02:41 +01:00
Sujay Jayakar 4d7e81e0ab Add Stream-Safe Text Process (for interop with golang's unicode/norm package) 2018-05-01 21:43:04 -07:00
Sujay Jayakar 6a7e38e376 Refactor Unicode script, emit new tables, and add QuickCheck algorithm. 2018-05-01 21:00:38 -07:00
Behnam Esfahbod 1e74c347b6 Fix is_combining_mark table data
* In `scripts/unicode.py`, the data used to generate `is_combining_mark()`
was being passed to the emit function incorrectly, resulting in the
table containing some other data instead. The script is fixed and new
`tables.rs` is generated.

* Add test for `is_combining_mark()` for ASCII chars, as well as a
couple of random chars based on the reported issue.

Fix https://github.com/unicode-rs/unicode-normalization/issues/16
2017-06-01 15:10:29 -06:00
Nathan Froyd 8d01bc5e88 store smaller slices in unicode data tables
Rust's default slices are convenient, but for tables like:

  const f: &'static [(char, &'static [char])]

they take up far too much space.  An element of the above array consumes
24 bytes on 64-bit platforms, and unicode-normalization contains about
6000 such array elements.

A better approach is to manually store a smaller slice type:

  struct Slice {
    offset: u16,
    length: u16,
  }

  const f: &'static [(char, Slice)]

and store the actual character data in a separate array on the side.
The `Slice` structures then point in to this separate array, but at a
much smaller space cost: elements of the modified `f` take up only 8
bytes on 64-bit platforms, which implies a space savings of ~96K on
64-bit platforms.  On some systems, this strategy also eliminates the
necessity of run-time relocations, which can be a further, significant
savings in binary size and runtime cost.

This change is strictly local to the library; it does not affect the
public API.
2017-03-27 16:58:31 -04:00
Nathan Froyd 7552e6cd2b format tables with one entry per line
This change avoids formatting table entries into a string, only to split
them apart again.  The new format is also slightly easier to read and
compare when changes are made to how the tables are organized.
2017-03-27 15:13:36 -04:00
Nathan Froyd 6190241cc9 use more idiomatic python in unicode.py
STR.join(...) is better than manual `for` loops that remember whether
we've processed the first element.
2017-03-27 14:26:35 -04:00
Simon Sapin 5d0443bd92 Add a char::is_combining_mark function.
This is used for international domain name validation:
http://unicode.org/reports/tr46/#Validity_Criteria

> The label must not begin with a combining mark, that is: General_Category=Mark.
2016-01-15 17:49:39 +01:00
kwantam e0d33a8661 make load_properties() tolerate whitespace
This commit imports update to regexes in `load_properties()`,
d25c39f86568a147f9b7080c25711fb1f98f056a in rust-lang/regex

Does not result in any changes to tables.rs, but could if
published tables are updated in the future.
2015-04-16 16:11:04 -04:00
kwantam c7a8bcfdc5 make sure unicode.py puts out optimized tables 2015-04-15 20:04:41 -04:00
kwantam 3d02ca9e05 tiny doc touch 2015-04-15 16:03:36 -04:00
kwantam 7a49d257eb add Unicode norms test suite; bump to ver 0.0.3 2015-04-15 15:40:06 -04:00
kwantam 933f988530 add tablegen script and tables.rs 2015-04-14 21:22:32 -04:00