Skip to content

Uniform float improvements - #1289

Merged
vks merged 6 commits into
rust-random:masterfrom
dhardy:uniform-float
May 1, 2023
Merged

Uniform float improvements#1289
vks merged 6 commits into
rust-random:masterfrom
dhardy:uniform-float

Conversation

@dhardy

Copy link
Copy Markdown
Member
  • Add a (possibly overcomplicated) benchmark for uniform float sampling
  • Add a couple extra tests
  • Add an explicit impl of sample_single_inclusive (~20% perf increase)

Note: sample_single and sample_single_inclusive use
different code paths for sampling.
Around 20% faster for f32, slightly less for f64.
@dhardy

Copy link
Copy Markdown
MemberAuthor

Closes #1270. This branch was created to evaluate the mentioned paper: https://hal.science/hal-03282794/document

The paper ignores possible underflow, but notes the following potential issues for uniform FP sampling from a defined range:

  • Available precision may vary throughout the given range due to differing exponent
  • The method we use to sample from [0, 1) can only sample half the possible values just below 1
  • Translating from x in [0,1) to a range [a, b) via common methods (such as a + (b-a)*x) can yield value b due to loss of precision. We have special measures for dealing with this.
  • b - a may overflow. (Note: I personally do not think this a big deal since it is rare to use FP values near the limits of their range.)
  • Most transformations do not produce equidistant FP values. In some cases such as Monte Carlo estimation of PI this can impact the result, but in practice I believe it is rarely an issue.

GSL's method, a(1-x) + bx, translates to a(2-z) + b(z-1) where our samples are z in [1, 2) and x = z - 1. This method was benchmarked, with approx 12% performance loss for f32 single-sampling, 2% loss for f64, 10% loss for f32 distribution sampling, and 19% loss for f64. Only 3 of 24 benchmarks showed a performance gain. An attraction of this method is that it avoids the possible overflow of calculating b - a.

The method using y = 2(a/2 + (b/2 - a/2)x) has accuracy issues on sub-normals. In particular, something like f32::from_bits(3) / 2.0 rounds up, thus the result may exceed the upper bound. This is a more significant issue than overflow, hence this method is disqualified from further analysis.

The paper defines method SESS (Same Exponent Same Sign) which we shall ignore due to the narrow circumstances in which it is applicable.

The paper also defines method γsection for general applicability. This is used for sample_single_inclusive, translated to Rust from the Julia snippet γsectionCC as follows:

let(a, b) = (low, high);let g = (a.next_up() - a).max(b - b.next_down());let s = b/g - a/g;let eps = if a.abs() <= b.abs(){
-a/g - (s - b/g)}else{
b /g - (s + a/g)};let si = s.ceil()as $uty;let hi = if s != si as $ty {
si
}else{
si + (eps > 0.0)as $uty
};let k = (0..=hi).sample_single(rng).unwrap();Ok(if a.abs() <= b.abs(){if k == hi {
a
}else{
b - (k as $ty)*g
}}else{if k == hi {
b
}else{
a + (k as $ty)*g
}})

Results are around 10 times slower for f32 and 6 times for f64:

$ cargo bench --bench uniform_float --features small_rng -- -b baseline inclusive
Compiling rand v0.9.0 (/home/dhardy/projects/rand/rand)
Finished bench [optimized] target(s) in 1.47s
Running benches/uniform_float.rs (target/release/deps/uniform_float-177b17dd872a054a)
single_random_f32/SmallRng/sample_inclusive
time: [17.293 ns 17.337 ns 17.377 ns]
change: [+928.42% +930.48% +934.08%] (p = 0.00 < 0.05)
Performance has regressed.
Found 7868 outliers among 100000 measurements (7.87%)
7868 (7.87%) high mild
single_random_f32/ChaCha8Rng/sample_inclusive
time: [18.027 ns 18.072 ns 18.118 ns]
change: [+832.83% +835.21% +837.39%] (p = 0.00 < 0.05)
Performance has regressed.
Found 5987 outliers among 100000 measurements (5.99%)
5987 (5.99%) high mild
single_random_f32/Pcg32/sample_inclusive
time: [17.056 ns 17.096 ns 17.139 ns]
change: [+913.05% +916.00% +918.69%] (p = 0.00 < 0.05)
Performance has regressed.
Found 7752 outliers among 100000 measurements (7.75%)
7752 (7.75%) high mild
single_random_f32/Pcg64/sample_inclusive
time: [17.796 ns 17.838 ns 17.880 ns]
change: [+893.31% +895.96% +898.01%] (p = 0.00 < 0.05)
Performance has regressed.
Found 5205 outliers among 100000 measurements (5.21%)
5205 (5.21%) high mild
single_random_f64/SmallRng/sample_inclusive
time: [22.094 ns 22.143 ns 22.189 ns]
change: [+571.29% +572.87% +574.19%] (p = 0.00 < 0.05)
Performance has regressed.
Found 6735 outliers among 100000 measurements (6.74%)
6735 (6.74%) high mild
single_random_f64/ChaCha8Rng/sample_inclusive
time: [23.074 ns 23.124 ns 23.173 ns]
change: [+445.36% +446.46% +447.50%] (p = 0.00 < 0.05)
Performance has regressed.
Found 3917 outliers among 100000 measurements (3.92%)
3917 (3.92%) high mild
single_random_f64/Pcg32/sample_inclusive
time: [22.403 ns 22.448 ns 22.493 ns]
change: [+570.28% +571.57% +573.10%] (p = 0.00 < 0.05)
Performance has regressed.
Found 4182 outliers among 100000 measurements (4.18%)
4182 (4.18%) high mild
single_random_f64/Pcg64/sample_inclusive
time: [22.518 ns 22.565 ns 22.611 ns]
change: [+518.80% +520.10% +521.35%] (p = 0.00 < 0.05)
Performance has regressed.
Found 5656 outliers among 100000 measurements (5.66%)
5656 (5.66%) high mild

In comparison, our current implementations benchmark at 1-1.5ns for distribution sampling, ~2ns for single f32 sampling and ~4ns for single f64 sampling on an AMD 5800X at 3.8GHz.

The paper's implementation of SESS benchmarked at 10ns per value for both f32 and f64, while most other methods benchmarked at approx 15ns per value on an Intel 6700HQ at 2.6GHz. It would appear that there is room for optimisation both in the SESS implementation above and in the other implementations as used in the paper, however I am doubtful that the method could perform competitively with our current method.

Note also that #531 should (if I remember correctly) be able to offer more precise sampling and with similar performance, thus I do not see much benefit in considering the γsection method further.

@dhardy

Copy link
Copy Markdown
MemberAuthor

Test failure should be resolved by #1287 (use of feature small_rng for testing benchmarks).

@vks could you review please?

@TheIronBorn

TheIronBorn commented Feb 22, 2023

Copy link
Copy Markdown
Contributor

Are there any significant optimizations we can make for the [0,1) case?

@dhardy

Copy link
Copy Markdown
MemberAuthor

I didn't look at that here, but I don't think so (we essentially just mask and bit-cast an integer, then subtract 1).

Comment threadbenches/uniform_float.rs Outdated

@vksvks left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good! I only have some small questions about the benchmarks.

Comment threadbenches/uniform_float.rs
Comment threadbenches/uniform_float.rs
Comment threadsrc/distributions/uniform.rs Outdated
Also avoid using internal API of Uniform distribution.
vks
vks approved these changes May 1, 2023
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@dhardy@TheIronBorn@vks
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Uniform float improvements by dhardy · Pull Request #1289 · rust-random/rand · GitHub
Skip to content

Uniform float improvements - #1289

Merged
vks merged 6 commits into
rust-random:masterfrom
dhardy:uniform-float
May 1, 2023
Merged

Uniform float improvements#1289
vks merged 6 commits into
rust-random:masterfrom
dhardy:uniform-float

Conversation

@dhardy

Copy link
Copy Markdown
Member
  • Add a (possibly overcomplicated) benchmark for uniform float sampling
  • Add a couple extra tests
  • Add an explicit impl of sample_single_inclusive (~20% perf increase)

Note: sample_single and sample_single_inclusive use
different code paths for sampling.
Around 20% faster for f32, slightly less for f64.
@dhardy

Copy link
Copy Markdown
MemberAuthor

Closes #1270. This branch was created to evaluate the mentioned paper: https://hal.science/hal-03282794/document

The paper ignores possible underflow, but notes the following potential issues for uniform FP sampling from a defined range:

  • Available precision may vary throughout the given range due to differing exponent
  • The method we use to sample from [0, 1) can only sample half the possible values just below 1
  • Translating from x in [0,1) to a range [a, b) via common methods (such as a + (b-a)*x) can yield value b due to loss of precision. We have special measures for dealing with this.
  • b - a may overflow. (Note: I personally do not think this a big deal since it is rare to use FP values near the limits of their range.)
  • Most transformations do not produce equidistant FP values. In some cases such as Monte Carlo estimation of PI this can impact the result, but in practice I believe it is rarely an issue.

GSL's method, a(1-x) + bx, translates to a(2-z) + b(z-1) where our samples are z in [1, 2) and x = z - 1. This method was benchmarked, with approx 12% performance loss for f32 single-sampling, 2% loss for f64, 10% loss for f32 distribution sampling, and 19% loss for f64. Only 3 of 24 benchmarks showed a performance gain. An attraction of this method is that it avoids the possible overflow of calculating b - a.

The method using y = 2(a/2 + (b/2 - a/2)x) has accuracy issues on sub-normals. In particular, something like f32::from_bits(3) / 2.0 rounds up, thus the result may exceed the upper bound. This is a more significant issue than overflow, hence this method is disqualified from further analysis.

The paper defines method SESS (Same Exponent Same Sign) which we shall ignore due to the narrow circumstances in which it is applicable.

The paper also defines method γsection for general applicability. This is used for sample_single_inclusive, translated to Rust from the Julia snippet γsectionCC as follows:

let(a, b) = (low, high);let g = (a.next_up() - a).max(b - b.next_down());let s = b/g - a/g;let eps = if a.abs() <= b.abs(){
-a/g - (s - b/g)}else{
b /g - (s + a/g)};let si = s.ceil()as $uty;let hi = if s != si as $ty {
si
}else{
si + (eps > 0.0)as $uty
};let k = (0..=hi).sample_single(rng).unwrap();Ok(if a.abs() <= b.abs(){if k == hi {
a
}else{
b - (k as $ty)*g
}}else{if k == hi {
b
}else{
a + (k as $ty)*g
}})

Results are around 10 times slower for f32 and 6 times for f64:

$ cargo bench --bench uniform_float --features small_rng -- -b baseline inclusive
Compiling rand v0.9.0 (/home/dhardy/projects/rand/rand)
Finished bench [optimized] target(s) in 1.47s
Running benches/uniform_float.rs (target/release/deps/uniform_float-177b17dd872a054a)
single_random_f32/SmallRng/sample_inclusive
time: [17.293 ns 17.337 ns 17.377 ns]
change: [+928.42% +930.48% +934.08%] (p = 0.00 < 0.05)
Performance has regressed.
Found 7868 outliers among 100000 measurements (7.87%)
7868 (7.87%) high mild
single_random_f32/ChaCha8Rng/sample_inclusive
time: [18.027 ns 18.072 ns 18.118 ns]
change: [+832.83% +835.21% +837.39%] (p = 0.00 < 0.05)
Performance has regressed.
Found 5987 outliers among 100000 measurements (5.99%)
5987 (5.99%) high mild
single_random_f32/Pcg32/sample_inclusive
time: [17.056 ns 17.096 ns 17.139 ns]
change: [+913.05% +916.00% +918.69%] (p = 0.00 < 0.05)
Performance has regressed.
Found 7752 outliers among 100000 measurements (7.75%)
7752 (7.75%) high mild
single_random_f32/Pcg64/sample_inclusive
time: [17.796 ns 17.838 ns 17.880 ns]
change: [+893.31% +895.96% +898.01%] (p = 0.00 < 0.05)
Performance has regressed.
Found 5205 outliers among 100000 measurements (5.21%)
5205 (5.21%) high mild
single_random_f64/SmallRng/sample_inclusive
time: [22.094 ns 22.143 ns 22.189 ns]
change: [+571.29% +572.87% +574.19%] (p = 0.00 < 0.05)
Performance has regressed.
Found 6735 outliers among 100000 measurements (6.74%)
6735 (6.74%) high mild
single_random_f64/ChaCha8Rng/sample_inclusive
time: [23.074 ns 23.124 ns 23.173 ns]
change: [+445.36% +446.46% +447.50%] (p = 0.00 < 0.05)
Performance has regressed.
Found 3917 outliers among 100000 measurements (3.92%)
3917 (3.92%) high mild
single_random_f64/Pcg32/sample_inclusive
time: [22.403 ns 22.448 ns 22.493 ns]
change: [+570.28% +571.57% +573.10%] (p = 0.00 < 0.05)
Performance has regressed.
Found 4182 outliers among 100000 measurements (4.18%)
4182 (4.18%) high mild
single_random_f64/Pcg64/sample_inclusive
time: [22.518 ns 22.565 ns 22.611 ns]
change: [+518.80% +520.10% +521.35%] (p = 0.00 < 0.05)
Performance has regressed.
Found 5656 outliers among 100000 measurements (5.66%)
5656 (5.66%) high mild

In comparison, our current implementations benchmark at 1-1.5ns for distribution sampling, ~2ns for single f32 sampling and ~4ns for single f64 sampling on an AMD 5800X at 3.8GHz.

The paper's implementation of SESS benchmarked at 10ns per value for both f32 and f64, while most other methods benchmarked at approx 15ns per value on an Intel 6700HQ at 2.6GHz. It would appear that there is room for optimisation both in the SESS implementation above and in the other implementations as used in the paper, however I am doubtful that the method could perform competitively with our current method.

Note also that #531 should (if I remember correctly) be able to offer more precise sampling and with similar performance, thus I do not see much benefit in considering the γsection method further.

@dhardy

Copy link
Copy Markdown
MemberAuthor

Test failure should be resolved by #1287 (use of feature small_rng for testing benchmarks).

@vks could you review please?

@TheIronBorn

TheIronBorn commented Feb 22, 2023

Copy link
Copy Markdown
Contributor

Are there any significant optimizations we can make for the [0,1) case?

@dhardy

Copy link
Copy Markdown
MemberAuthor

I didn't look at that here, but I don't think so (we essentially just mask and bit-cast an integer, then subtract 1).

Comment threadbenches/uniform_float.rs Outdated

@vksvks left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good! I only have some small questions about the benchmarks.

Comment threadbenches/uniform_float.rs
Comment threadbenches/uniform_float.rs
Comment threadsrc/distributions/uniform.rs Outdated
Also avoid using internal API of Uniform distribution.
vks
vks approved these changes May 1, 2023
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@dhardy@TheIronBorn@vks
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Uniform float improvements by dhardy · Pull Request #1289 · rust-random/rand · GitHub
Skip to content

Uniform float improvements - #1289

Merged
vks merged 6 commits into
rust-random:masterfrom
dhardy:uniform-float
May 1, 2023
Merged

Uniform float improvements#1289
vks merged 6 commits into
rust-random:masterfrom
dhardy:uniform-float

Conversation

@dhardy

Copy link
Copy Markdown
Member
  • Add a (possibly overcomplicated) benchmark for uniform float sampling
  • Add a couple extra tests
  • Add an explicit impl of sample_single_inclusive (~20% perf increase)

Note: sample_single and sample_single_inclusive use
different code paths for sampling.
Around 20% faster for f32, slightly less for f64.
@dhardy

Copy link
Copy Markdown
MemberAuthor

Closes #1270. This branch was created to evaluate the mentioned paper: https://hal.science/hal-03282794/document

The paper ignores possible underflow, but notes the following potential issues for uniform FP sampling from a defined range:

  • Available precision may vary throughout the given range due to differing exponent
  • The method we use to sample from [0, 1) can only sample half the possible values just below 1
  • Translating from x in [0,1) to a range [a, b) via common methods (such as a + (b-a)*x) can yield value b due to loss of precision. We have special measures for dealing with this.
  • b - a may overflow. (Note: I personally do not think this a big deal since it is rare to use FP values near the limits of their range.)
  • Most transformations do not produce equidistant FP values. In some cases such as Monte Carlo estimation of PI this can impact the result, but in practice I believe it is rarely an issue.

GSL's method, a(1-x) + bx, translates to a(2-z) + b(z-1) where our samples are z in [1, 2) and x = z - 1. This method was benchmarked, with approx 12% performance loss for f32 single-sampling, 2% loss for f64, 10% loss for f32 distribution sampling, and 19% loss for f64. Only 3 of 24 benchmarks showed a performance gain. An attraction of this method is that it avoids the possible overflow of calculating b - a.

The method using y = 2(a/2 + (b/2 - a/2)x) has accuracy issues on sub-normals. In particular, something like f32::from_bits(3) / 2.0 rounds up, thus the result may exceed the upper bound. This is a more significant issue than overflow, hence this method is disqualified from further analysis.

The paper defines method SESS (Same Exponent Same Sign) which we shall ignore due to the narrow circumstances in which it is applicable.

The paper also defines method γsection for general applicability. This is used for sample_single_inclusive, translated to Rust from the Julia snippet γsectionCC as follows:

let(a, b) = (low, high);let g = (a.next_up() - a).max(b - b.next_down());let s = b/g - a/g;let eps = if a.abs() <= b.abs(){
-a/g - (s - b/g)}else{
b /g - (s + a/g)};let si = s.ceil()as $uty;let hi = if s != si as $ty {
si
}else{
si + (eps > 0.0)as $uty
};let k = (0..=hi).sample_single(rng).unwrap();Ok(if a.abs() <= b.abs(){if k == hi {
a
}else{
b - (k as $ty)*g
}}else{if k == hi {
b
}else{
a + (k as $ty)*g
}})

Results are around 10 times slower for f32 and 6 times for f64:

$ cargo bench --bench uniform_float --features small_rng -- -b baseline inclusive
Compiling rand v0.9.0 (/home/dhardy/projects/rand/rand)
Finished bench [optimized] target(s) in 1.47s
Running benches/uniform_float.rs (target/release/deps/uniform_float-177b17dd872a054a)
single_random_f32/SmallRng/sample_inclusive
time: [17.293 ns 17.337 ns 17.377 ns]
change: [+928.42% +930.48% +934.08%] (p = 0.00 < 0.05)
Performance has regressed.
Found 7868 outliers among 100000 measurements (7.87%)
7868 (7.87%) high mild
single_random_f32/ChaCha8Rng/sample_inclusive
time: [18.027 ns 18.072 ns 18.118 ns]
change: [+832.83% +835.21% +837.39%] (p = 0.00 < 0.05)
Performance has regressed.
Found 5987 outliers among 100000 measurements (5.99%)
5987 (5.99%) high mild
single_random_f32/Pcg32/sample_inclusive
time: [17.056 ns 17.096 ns 17.139 ns]
change: [+913.05% +916.00% +918.69%] (p = 0.00 < 0.05)
Performance has regressed.
Found 7752 outliers among 100000 measurements (7.75%)
7752 (7.75%) high mild
single_random_f32/Pcg64/sample_inclusive
time: [17.796 ns 17.838 ns 17.880 ns]
change: [+893.31% +895.96% +898.01%] (p = 0.00 < 0.05)
Performance has regressed.
Found 5205 outliers among 100000 measurements (5.21%)
5205 (5.21%) high mild
single_random_f64/SmallRng/sample_inclusive
time: [22.094 ns 22.143 ns 22.189 ns]
change: [+571.29% +572.87% +574.19%] (p = 0.00 < 0.05)
Performance has regressed.
Found 6735 outliers among 100000 measurements (6.74%)
6735 (6.74%) high mild
single_random_f64/ChaCha8Rng/sample_inclusive
time: [23.074 ns 23.124 ns 23.173 ns]
change: [+445.36% +446.46% +447.50%] (p = 0.00 < 0.05)
Performance has regressed.
Found 3917 outliers among 100000 measurements (3.92%)
3917 (3.92%) high mild
single_random_f64/Pcg32/sample_inclusive
time: [22.403 ns 22.448 ns 22.493 ns]
change: [+570.28% +571.57% +573.10%] (p = 0.00 < 0.05)
Performance has regressed.
Found 4182 outliers among 100000 measurements (4.18%)
4182 (4.18%) high mild
single_random_f64/Pcg64/sample_inclusive
time: [22.518 ns 22.565 ns 22.611 ns]
change: [+518.80% +520.10% +521.35%] (p = 0.00 < 0.05)
Performance has regressed.
Found 5656 outliers among 100000 measurements (5.66%)
5656 (5.66%) high mild

In comparison, our current implementations benchmark at 1-1.5ns for distribution sampling, ~2ns for single f32 sampling and ~4ns for single f64 sampling on an AMD 5800X at 3.8GHz.

The paper's implementation of SESS benchmarked at 10ns per value for both f32 and f64, while most other methods benchmarked at approx 15ns per value on an Intel 6700HQ at 2.6GHz. It would appear that there is room for optimisation both in the SESS implementation above and in the other implementations as used in the paper, however I am doubtful that the method could perform competitively with our current method.

Note also that #531 should (if I remember correctly) be able to offer more precise sampling and with similar performance, thus I do not see much benefit in considering the γsection method further.

@dhardy

Copy link
Copy Markdown
MemberAuthor

Test failure should be resolved by #1287 (use of feature small_rng for testing benchmarks).

@vks could you review please?

@TheIronBorn

TheIronBorn commented Feb 22, 2023

Copy link
Copy Markdown
Contributor

Are there any significant optimizations we can make for the [0,1) case?

@dhardy

Copy link
Copy Markdown
MemberAuthor

I didn't look at that here, but I don't think so (we essentially just mask and bit-cast an integer, then subtract 1).

Comment threadbenches/uniform_float.rs Outdated

@vksvks left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good! I only have some small questions about the benchmarks.

Comment threadbenches/uniform_float.rs
Comment threadbenches/uniform_float.rs
Comment threadsrc/distributions/uniform.rs Outdated
Also avoid using internal API of Uniform distribution.
vks
vks approved these changes May 1, 2023
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@dhardy@TheIronBorn@vks
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Uniform float improvements by dhardy · Pull Request #1289 · rust-random/rand · GitHub
Skip to content

Uniform float improvements - #1289

Merged
vks merged 6 commits into
rust-random:masterfrom
dhardy:uniform-float
May 1, 2023
Merged

Uniform float improvements#1289
vks merged 6 commits into
rust-random:masterfrom
dhardy:uniform-float

Conversation

@dhardy

Copy link
Copy Markdown
Member
  • Add a (possibly overcomplicated) benchmark for uniform float sampling
  • Add a couple extra tests
  • Add an explicit impl of sample_single_inclusive (~20% perf increase)

Note: sample_single and sample_single_inclusive use
different code paths for sampling.
Around 20% faster for f32, slightly less for f64.
@dhardy

Copy link
Copy Markdown
MemberAuthor

Closes #1270. This branch was created to evaluate the mentioned paper: https://hal.science/hal-03282794/document

The paper ignores possible underflow, but notes the following potential issues for uniform FP sampling from a defined range:

  • Available precision may vary throughout the given range due to differing exponent
  • The method we use to sample from [0, 1) can only sample half the possible values just below 1
  • Translating from x in [0,1) to a range [a, b) via common methods (such as a + (b-a)*x) can yield value b due to loss of precision. We have special measures for dealing with this.
  • b - a may overflow. (Note: I personally do not think this a big deal since it is rare to use FP values near the limits of their range.)
  • Most transformations do not produce equidistant FP values. In some cases such as Monte Carlo estimation of PI this can impact the result, but in practice I believe it is rarely an issue.

GSL's method, a(1-x) + bx, translates to a(2-z) + b(z-1) where our samples are z in [1, 2) and x = z - 1. This method was benchmarked, with approx 12% performance loss for f32 single-sampling, 2% loss for f64, 10% loss for f32 distribution sampling, and 19% loss for f64. Only 3 of 24 benchmarks showed a performance gain. An attraction of this method is that it avoids the possible overflow of calculating b - a.

The method using y = 2(a/2 + (b/2 - a/2)x) has accuracy issues on sub-normals. In particular, something like f32::from_bits(3) / 2.0 rounds up, thus the result may exceed the upper bound. This is a more significant issue than overflow, hence this method is disqualified from further analysis.

The paper defines method SESS (Same Exponent Same Sign) which we shall ignore due to the narrow circumstances in which it is applicable.

The paper also defines method γsection for general applicability. This is used for sample_single_inclusive, translated to Rust from the Julia snippet γsectionCC as follows:

let(a, b) = (low, high);let g = (a.next_up() - a).max(b - b.next_down());let s = b/g - a/g;let eps = if a.abs() <= b.abs(){
-a/g - (s - b/g)}else{
b /g - (s + a/g)};let si = s.ceil()as $uty;let hi = if s != si as $ty {
si
}else{
si + (eps > 0.0)as $uty
};let k = (0..=hi).sample_single(rng).unwrap();Ok(if a.abs() <= b.abs(){if k == hi {
a
}else{
b - (k as $ty)*g
}}else{if k == hi {
b
}else{
a + (k as $ty)*g
}})

Results are around 10 times slower for f32 and 6 times for f64:

$ cargo bench --bench uniform_float --features small_rng -- -b baseline inclusive
Compiling rand v0.9.0 (/home/dhardy/projects/rand/rand)
Finished bench [optimized] target(s) in 1.47s
Running benches/uniform_float.rs (target/release/deps/uniform_float-177b17dd872a054a)
single_random_f32/SmallRng/sample_inclusive
time: [17.293 ns 17.337 ns 17.377 ns]
change: [+928.42% +930.48% +934.08%] (p = 0.00 < 0.05)
Performance has regressed.
Found 7868 outliers among 100000 measurements (7.87%)
7868 (7.87%) high mild
single_random_f32/ChaCha8Rng/sample_inclusive
time: [18.027 ns 18.072 ns 18.118 ns]
change: [+832.83% +835.21% +837.39%] (p = 0.00 < 0.05)
Performance has regressed.
Found 5987 outliers among 100000 measurements (5.99%)
5987 (5.99%) high mild
single_random_f32/Pcg32/sample_inclusive
time: [17.056 ns 17.096 ns 17.139 ns]
change: [+913.05% +916.00% +918.69%] (p = 0.00 < 0.05)
Performance has regressed.
Found 7752 outliers among 100000 measurements (7.75%)
7752 (7.75%) high mild
single_random_f32/Pcg64/sample_inclusive
time: [17.796 ns 17.838 ns 17.880 ns]
change: [+893.31% +895.96% +898.01%] (p = 0.00 < 0.05)
Performance has regressed.
Found 5205 outliers among 100000 measurements (5.21%)
5205 (5.21%) high mild
single_random_f64/SmallRng/sample_inclusive
time: [22.094 ns 22.143 ns 22.189 ns]
change: [+571.29% +572.87% +574.19%] (p = 0.00 < 0.05)
Performance has regressed.
Found 6735 outliers among 100000 measurements (6.74%)
6735 (6.74%) high mild
single_random_f64/ChaCha8Rng/sample_inclusive
time: [23.074 ns 23.124 ns 23.173 ns]
change: [+445.36% +446.46% +447.50%] (p = 0.00 < 0.05)
Performance has regressed.
Found 3917 outliers among 100000 measurements (3.92%)
3917 (3.92%) high mild
single_random_f64/Pcg32/sample_inclusive
time: [22.403 ns 22.448 ns 22.493 ns]
change: [+570.28% +571.57% +573.10%] (p = 0.00 < 0.05)
Performance has regressed.
Found 4182 outliers among 100000 measurements (4.18%)
4182 (4.18%) high mild
single_random_f64/Pcg64/sample_inclusive
time: [22.518 ns 22.565 ns 22.611 ns]
change: [+518.80% +520.10% +521.35%] (p = 0.00 < 0.05)
Performance has regressed.
Found 5656 outliers among 100000 measurements (5.66%)
5656 (5.66%) high mild

In comparison, our current implementations benchmark at 1-1.5ns for distribution sampling, ~2ns for single f32 sampling and ~4ns for single f64 sampling on an AMD 5800X at 3.8GHz.

The paper's implementation of SESS benchmarked at 10ns per value for both f32 and f64, while most other methods benchmarked at approx 15ns per value on an Intel 6700HQ at 2.6GHz. It would appear that there is room for optimisation both in the SESS implementation above and in the other implementations as used in the paper, however I am doubtful that the method could perform competitively with our current method.

Note also that #531 should (if I remember correctly) be able to offer more precise sampling and with similar performance, thus I do not see much benefit in considering the γsection method further.

@dhardy

Copy link
Copy Markdown
MemberAuthor

Test failure should be resolved by #1287 (use of feature small_rng for testing benchmarks).

@vks could you review please?

@TheIronBorn

TheIronBorn commented Feb 22, 2023

Copy link
Copy Markdown
Contributor

Are there any significant optimizations we can make for the [0,1) case?

@dhardy

Copy link
Copy Markdown
MemberAuthor

I didn't look at that here, but I don't think so (we essentially just mask and bit-cast an integer, then subtract 1).

Comment threadbenches/uniform_float.rs Outdated

@vksvks left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good! I only have some small questions about the benchmarks.

Comment threadbenches/uniform_float.rs
Comment threadbenches/uniform_float.rs
Comment threadsrc/distributions/uniform.rs Outdated
Also avoid using internal API of Uniform distribution.
vks
vks approved these changes May 1, 2023
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@dhardy@TheIronBorn@vks
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' Uniform float improvements by dhardy · Pull Request #1289 · rust-random/rand · GitHub
Skip to content

Uniform float improvements - #1289

Merged
vks merged 6 commits into
rust-random:masterfrom
dhardy:uniform-float
May 1, 2023
Merged

Uniform float improvements#1289
vks merged 6 commits into
rust-random:masterfrom
dhardy:uniform-float

Conversation

@dhardy

Copy link
Copy Markdown
Member
  • Add a (possibly overcomplicated) benchmark for uniform float sampling
  • Add a couple extra tests
  • Add an explicit impl of sample_single_inclusive (~20% perf increase)

Note: sample_single and sample_single_inclusive use
different code paths for sampling.
Around 20% faster for f32, slightly less for f64.
@dhardy

Copy link
Copy Markdown
MemberAuthor

Closes #1270. This branch was created to evaluate the mentioned paper: https://hal.science/hal-03282794/document

The paper ignores possible underflow, but notes the following potential issues for uniform FP sampling from a defined range:

  • Available precision may vary throughout the given range due to differing exponent
  • The method we use to sample from [0, 1) can only sample half the possible values just below 1
  • Translating from x in [0,1) to a range [a, b) via common methods (such as a + (b-a)*x) can yield value b due to loss of precision. We have special measures for dealing with this.
  • b - a may overflow. (Note: I personally do not think this a big deal since it is rare to use FP values near the limits of their range.)
  • Most transformations do not produce equidistant FP values. In some cases such as Monte Carlo estimation of PI this can impact the result, but in practice I believe it is rarely an issue.

GSL's method, a(1-x) + bx, translates to a(2-z) + b(z-1) where our samples are z in [1, 2) and x = z - 1. This method was benchmarked, with approx 12% performance loss for f32 single-sampling, 2% loss for f64, 10% loss for f32 distribution sampling, and 19% loss for f64. Only 3 of 24 benchmarks showed a performance gain. An attraction of this method is that it avoids the possible overflow of calculating b - a.

The method using y = 2(a/2 + (b/2 - a/2)x) has accuracy issues on sub-normals. In particular, something like f32::from_bits(3) / 2.0 rounds up, thus the result may exceed the upper bound. This is a more significant issue than overflow, hence this method is disqualified from further analysis.

The paper defines method SESS (Same Exponent Same Sign) which we shall ignore due to the narrow circumstances in which it is applicable.

The paper also defines method γsection for general applicability. This is used for sample_single_inclusive, translated to Rust from the Julia snippet γsectionCC as follows:

let(a, b) = (low, high);let g = (a.next_up() - a).max(b - b.next_down());let s = b/g - a/g;let eps = if a.abs() <= b.abs(){
-a/g - (s - b/g)}else{
b /g - (s + a/g)};let si = s.ceil()as $uty;let hi = if s != si as $ty {
si
}else{
si + (eps > 0.0)as $uty
};let k = (0..=hi).sample_single(rng).unwrap();Ok(if a.abs() <= b.abs(){if k == hi {
a
}else{
b - (k as $ty)*g
}}else{if k == hi {
b
}else{
a + (k as $ty)*g
}})

Results are around 10 times slower for f32 and 6 times for f64:

$ cargo bench --bench uniform_float --features small_rng -- -b baseline inclusive
Compiling rand v0.9.0 (/home/dhardy/projects/rand/rand)
Finished bench [optimized] target(s) in 1.47s
Running benches/uniform_float.rs (target/release/deps/uniform_float-177b17dd872a054a)
single_random_f32/SmallRng/sample_inclusive
time: [17.293 ns 17.337 ns 17.377 ns]
change: [+928.42% +930.48% +934.08%] (p = 0.00 < 0.05)
Performance has regressed.
Found 7868 outliers among 100000 measurements (7.87%)
7868 (7.87%) high mild
single_random_f32/ChaCha8Rng/sample_inclusive
time: [18.027 ns 18.072 ns 18.118 ns]
change: [+832.83% +835.21% +837.39%] (p = 0.00 < 0.05)
Performance has regressed.
Found 5987 outliers among 100000 measurements (5.99%)
5987 (5.99%) high mild
single_random_f32/Pcg32/sample_inclusive
time: [17.056 ns 17.096 ns 17.139 ns]
change: [+913.05% +916.00% +918.69%] (p = 0.00 < 0.05)
Performance has regressed.
Found 7752 outliers among 100000 measurements (7.75%)
7752 (7.75%) high mild
single_random_f32/Pcg64/sample_inclusive
time: [17.796 ns 17.838 ns 17.880 ns]
change: [+893.31% +895.96% +898.01%] (p = 0.00 < 0.05)
Performance has regressed.
Found 5205 outliers among 100000 measurements (5.21%)
5205 (5.21%) high mild
single_random_f64/SmallRng/sample_inclusive
time: [22.094 ns 22.143 ns 22.189 ns]
change: [+571.29% +572.87% +574.19%] (p = 0.00 < 0.05)
Performance has regressed.
Found 6735 outliers among 100000 measurements (6.74%)
6735 (6.74%) high mild
single_random_f64/ChaCha8Rng/sample_inclusive
time: [23.074 ns 23.124 ns 23.173 ns]
change: [+445.36% +446.46% +447.50%] (p = 0.00 < 0.05)
Performance has regressed.
Found 3917 outliers among 100000 measurements (3.92%)
3917 (3.92%) high mild
single_random_f64/Pcg32/sample_inclusive
time: [22.403 ns 22.448 ns 22.493 ns]
change: [+570.28% +571.57% +573.10%] (p = 0.00 < 0.05)
Performance has regressed.
Found 4182 outliers among 100000 measurements (4.18%)
4182 (4.18%) high mild
single_random_f64/Pcg64/sample_inclusive
time: [22.518 ns 22.565 ns 22.611 ns]
change: [+518.80% +520.10% +521.35%] (p = 0.00 < 0.05)
Performance has regressed.
Found 5656 outliers among 100000 measurements (5.66%)
5656 (5.66%) high mild

In comparison, our current implementations benchmark at 1-1.5ns for distribution sampling, ~2ns for single f32 sampling and ~4ns for single f64 sampling on an AMD 5800X at 3.8GHz.

The paper's implementation of SESS benchmarked at 10ns per value for both f32 and f64, while most other methods benchmarked at approx 15ns per value on an Intel 6700HQ at 2.6GHz. It would appear that there is room for optimisation both in the SESS implementation above and in the other implementations as used in the paper, however I am doubtful that the method could perform competitively with our current method.

Note also that #531 should (if I remember correctly) be able to offer more precise sampling and with similar performance, thus I do not see much benefit in considering the γsection method further.

@dhardy

Copy link
Copy Markdown
MemberAuthor

Test failure should be resolved by #1287 (use of feature small_rng for testing benchmarks).

@vks could you review please?

@TheIronBorn

TheIronBorn commented Feb 22, 2023

Copy link
Copy Markdown
Contributor

Are there any significant optimizations we can make for the [0,1) case?

@dhardy

Copy link
Copy Markdown
MemberAuthor

I didn't look at that here, but I don't think so (we essentially just mask and bit-cast an integer, then subtract 1).

Comment threadbenches/uniform_float.rs Outdated

@vksvks left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good! I only have some small questions about the benchmarks.

Comment threadbenches/uniform_float.rs
Comment threadbenches/uniform_float.rs
Comment threadsrc/distributions/uniform.rs Outdated
Also avoid using internal API of Uniform distribution.
vks
vks approved these changes May 1, 2023
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@dhardy@TheIronBorn@vks
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Uniform float improvements by dhardy · Pull Request #1289 · rust-random/rand · GitHub
Skip to content

Uniform float improvements - #1289

Merged
vks merged 6 commits into
rust-random:masterfrom
dhardy:uniform-float
May 1, 2023
Merged

Uniform float improvements#1289
vks merged 6 commits into
rust-random:masterfrom
dhardy:uniform-float

Conversation

@dhardy

Copy link
Copy Markdown
Member
  • Add a (possibly overcomplicated) benchmark for uniform float sampling
  • Add a couple extra tests
  • Add an explicit impl of sample_single_inclusive (~20% perf increase)

Note: sample_single and sample_single_inclusive use
different code paths for sampling.
Around 20% faster for f32, slightly less for f64.
@dhardy

Copy link
Copy Markdown
MemberAuthor

Closes #1270. This branch was created to evaluate the mentioned paper: https://hal.science/hal-03282794/document

The paper ignores possible underflow, but notes the following potential issues for uniform FP sampling from a defined range:

  • Available precision may vary throughout the given range due to differing exponent
  • The method we use to sample from [0, 1) can only sample half the possible values just below 1
  • Translating from x in [0,1) to a range [a, b) via common methods (such as a + (b-a)*x) can yield value b due to loss of precision. We have special measures for dealing with this.
  • b - a may overflow. (Note: I personally do not think this a big deal since it is rare to use FP values near the limits of their range.)
  • Most transformations do not produce equidistant FP values. In some cases such as Monte Carlo estimation of PI this can impact the result, but in practice I believe it is rarely an issue.

GSL's method, a(1-x) + bx, translates to a(2-z) + b(z-1) where our samples are z in [1, 2) and x = z - 1. This method was benchmarked, with approx 12% performance loss for f32 single-sampling, 2% loss for f64, 10% loss for f32 distribution sampling, and 19% loss for f64. Only 3 of 24 benchmarks showed a performance gain. An attraction of this method is that it avoids the possible overflow of calculating b - a.

The method using y = 2(a/2 + (b/2 - a/2)x) has accuracy issues on sub-normals. In particular, something like f32::from_bits(3) / 2.0 rounds up, thus the result may exceed the upper bound. This is a more significant issue than overflow, hence this method is disqualified from further analysis.

The paper defines method SESS (Same Exponent Same Sign) which we shall ignore due to the narrow circumstances in which it is applicable.

The paper also defines method γsection for general applicability. This is used for sample_single_inclusive, translated to Rust from the Julia snippet γsectionCC as follows:

let(a, b) = (low, high);let g = (a.next_up() - a).max(b - b.next_down());let s = b/g - a/g;let eps = if a.abs() <= b.abs(){
-a/g - (s - b/g)}else{
b /g - (s + a/g)};let si = s.ceil()as $uty;let hi = if s != si as $ty {
si
}else{
si + (eps > 0.0)as $uty
};let k = (0..=hi).sample_single(rng).unwrap();Ok(if a.abs() <= b.abs(){if k == hi {
a
}else{
b - (k as $ty)*g
}}else{if k == hi {
b
}else{
a + (k as $ty)*g
}})

Results are around 10 times slower for f32 and 6 times for f64:

$ cargo bench --bench uniform_float --features small_rng -- -b baseline inclusive
Compiling rand v0.9.0 (/home/dhardy/projects/rand/rand)
Finished bench [optimized] target(s) in 1.47s
Running benches/uniform_float.rs (target/release/deps/uniform_float-177b17dd872a054a)
single_random_f32/SmallRng/sample_inclusive
time: [17.293 ns 17.337 ns 17.377 ns]
change: [+928.42% +930.48% +934.08%] (p = 0.00 < 0.05)
Performance has regressed.
Found 7868 outliers among 100000 measurements (7.87%)
7868 (7.87%) high mild
single_random_f32/ChaCha8Rng/sample_inclusive
time: [18.027 ns 18.072 ns 18.118 ns]
change: [+832.83% +835.21% +837.39%] (p = 0.00 < 0.05)
Performance has regressed.
Found 5987 outliers among 100000 measurements (5.99%)
5987 (5.99%) high mild
single_random_f32/Pcg32/sample_inclusive
time: [17.056 ns 17.096 ns 17.139 ns]
change: [+913.05% +916.00% +918.69%] (p = 0.00 < 0.05)
Performance has regressed.
Found 7752 outliers among 100000 measurements (7.75%)
7752 (7.75%) high mild
single_random_f32/Pcg64/sample_inclusive
time: [17.796 ns 17.838 ns 17.880 ns]
change: [+893.31% +895.96% +898.01%] (p = 0.00 < 0.05)
Performance has regressed.
Found 5205 outliers among 100000 measurements (5.21%)
5205 (5.21%) high mild
single_random_f64/SmallRng/sample_inclusive
time: [22.094 ns 22.143 ns 22.189 ns]
change: [+571.29% +572.87% +574.19%] (p = 0.00 < 0.05)
Performance has regressed.
Found 6735 outliers among 100000 measurements (6.74%)
6735 (6.74%) high mild
single_random_f64/ChaCha8Rng/sample_inclusive
time: [23.074 ns 23.124 ns 23.173 ns]
change: [+445.36% +446.46% +447.50%] (p = 0.00 < 0.05)
Performance has regressed.
Found 3917 outliers among 100000 measurements (3.92%)
3917 (3.92%) high mild
single_random_f64/Pcg32/sample_inclusive
time: [22.403 ns 22.448 ns 22.493 ns]
change: [+570.28% +571.57% +573.10%] (p = 0.00 < 0.05)
Performance has regressed.
Found 4182 outliers among 100000 measurements (4.18%)
4182 (4.18%) high mild
single_random_f64/Pcg64/sample_inclusive
time: [22.518 ns 22.565 ns 22.611 ns]
change: [+518.80% +520.10% +521.35%] (p = 0.00 < 0.05)
Performance has regressed.
Found 5656 outliers among 100000 measurements (5.66%)
5656 (5.66%) high mild

In comparison, our current implementations benchmark at 1-1.5ns for distribution sampling, ~2ns for single f32 sampling and ~4ns for single f64 sampling on an AMD 5800X at 3.8GHz.

The paper's implementation of SESS benchmarked at 10ns per value for both f32 and f64, while most other methods benchmarked at approx 15ns per value on an Intel 6700HQ at 2.6GHz. It would appear that there is room for optimisation both in the SESS implementation above and in the other implementations as used in the paper, however I am doubtful that the method could perform competitively with our current method.

Note also that #531 should (if I remember correctly) be able to offer more precise sampling and with similar performance, thus I do not see much benefit in considering the γsection method further.

@dhardy

Copy link
Copy Markdown
MemberAuthor

Test failure should be resolved by #1287 (use of feature small_rng for testing benchmarks).

@vks could you review please?

@TheIronBorn

TheIronBorn commented Feb 22, 2023

Copy link
Copy Markdown
Contributor

Are there any significant optimizations we can make for the [0,1) case?

@dhardy

Copy link
Copy Markdown
MemberAuthor

I didn't look at that here, but I don't think so (we essentially just mask and bit-cast an integer, then subtract 1).

Comment threadbenches/uniform_float.rs Outdated

@vksvks left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good! I only have some small questions about the benchmarks.

Comment threadbenches/uniform_float.rs
Comment threadbenches/uniform_float.rs
Comment threadsrc/distributions/uniform.rs Outdated
Also avoid using internal API of Uniform distribution.
vks
vks approved these changes May 1, 2023
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@dhardy@TheIronBorn@vks
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Uniform float improvements by dhardy · Pull Request #1289 · rust-random/rand · GitHub
Skip to content

Uniform float improvements - #1289

Merged
vks merged 6 commits into
rust-random:masterfrom
dhardy:uniform-float
May 1, 2023
Merged

Uniform float improvements#1289
vks merged 6 commits into
rust-random:masterfrom
dhardy:uniform-float

Conversation

@dhardy

Copy link
Copy Markdown
Member
  • Add a (possibly overcomplicated) benchmark for uniform float sampling
  • Add a couple extra tests
  • Add an explicit impl of sample_single_inclusive (~20% perf increase)

Note: sample_single and sample_single_inclusive use
different code paths for sampling.
Around 20% faster for f32, slightly less for f64.
@dhardy

Copy link
Copy Markdown
MemberAuthor

Closes #1270. This branch was created to evaluate the mentioned paper: https://hal.science/hal-03282794/document

The paper ignores possible underflow, but notes the following potential issues for uniform FP sampling from a defined range:

  • Available precision may vary throughout the given range due to differing exponent
  • The method we use to sample from [0, 1) can only sample half the possible values just below 1
  • Translating from x in [0,1) to a range [a, b) via common methods (such as a + (b-a)*x) can yield value b due to loss of precision. We have special measures for dealing with this.
  • b - a may overflow. (Note: I personally do not think this a big deal since it is rare to use FP values near the limits of their range.)
  • Most transformations do not produce equidistant FP values. In some cases such as Monte Carlo estimation of PI this can impact the result, but in practice I believe it is rarely an issue.

GSL's method, a(1-x) + bx, translates to a(2-z) + b(z-1) where our samples are z in [1, 2) and x = z - 1. This method was benchmarked, with approx 12% performance loss for f32 single-sampling, 2% loss for f64, 10% loss for f32 distribution sampling, and 19% loss for f64. Only 3 of 24 benchmarks showed a performance gain. An attraction of this method is that it avoids the possible overflow of calculating b - a.

The method using y = 2(a/2 + (b/2 - a/2)x) has accuracy issues on sub-normals. In particular, something like f32::from_bits(3) / 2.0 rounds up, thus the result may exceed the upper bound. This is a more significant issue than overflow, hence this method is disqualified from further analysis.

The paper defines method SESS (Same Exponent Same Sign) which we shall ignore due to the narrow circumstances in which it is applicable.

The paper also defines method γsection for general applicability. This is used for sample_single_inclusive, translated to Rust from the Julia snippet γsectionCC as follows:

let(a, b) = (low, high);let g = (a.next_up() - a).max(b - b.next_down());let s = b/g - a/g;let eps = if a.abs() <= b.abs(){
-a/g - (s - b/g)}else{
b /g - (s + a/g)};let si = s.ceil()as $uty;let hi = if s != si as $ty {
si
}else{
si + (eps > 0.0)as $uty
};let k = (0..=hi).sample_single(rng).unwrap();Ok(if a.abs() <= b.abs(){if k == hi {
a
}else{
b - (k as $ty)*g
}}else{if k == hi {
b
}else{
a + (k as $ty)*g
}})

Results are around 10 times slower for f32 and 6 times for f64:

$ cargo bench --bench uniform_float --features small_rng -- -b baseline inclusive
Compiling rand v0.9.0 (/home/dhardy/projects/rand/rand)
Finished bench [optimized] target(s) in 1.47s
Running benches/uniform_float.rs (target/release/deps/uniform_float-177b17dd872a054a)
single_random_f32/SmallRng/sample_inclusive
time: [17.293 ns 17.337 ns 17.377 ns]
change: [+928.42% +930.48% +934.08%] (p = 0.00 < 0.05)
Performance has regressed.
Found 7868 outliers among 100000 measurements (7.87%)
7868 (7.87%) high mild
single_random_f32/ChaCha8Rng/sample_inclusive
time: [18.027 ns 18.072 ns 18.118 ns]
change: [+832.83% +835.21% +837.39%] (p = 0.00 < 0.05)
Performance has regressed.
Found 5987 outliers among 100000 measurements (5.99%)
5987 (5.99%) high mild
single_random_f32/Pcg32/sample_inclusive
time: [17.056 ns 17.096 ns 17.139 ns]
change: [+913.05% +916.00% +918.69%] (p = 0.00 < 0.05)
Performance has regressed.
Found 7752 outliers among 100000 measurements (7.75%)
7752 (7.75%) high mild
single_random_f32/Pcg64/sample_inclusive
time: [17.796 ns 17.838 ns 17.880 ns]
change: [+893.31% +895.96% +898.01%] (p = 0.00 < 0.05)
Performance has regressed.
Found 5205 outliers among 100000 measurements (5.21%)
5205 (5.21%) high mild
single_random_f64/SmallRng/sample_inclusive
time: [22.094 ns 22.143 ns 22.189 ns]
change: [+571.29% +572.87% +574.19%] (p = 0.00 < 0.05)
Performance has regressed.
Found 6735 outliers among 100000 measurements (6.74%)
6735 (6.74%) high mild
single_random_f64/ChaCha8Rng/sample_inclusive
time: [23.074 ns 23.124 ns 23.173 ns]
change: [+445.36% +446.46% +447.50%] (p = 0.00 < 0.05)
Performance has regressed.
Found 3917 outliers among 100000 measurements (3.92%)
3917 (3.92%) high mild
single_random_f64/Pcg32/sample_inclusive
time: [22.403 ns 22.448 ns 22.493 ns]
change: [+570.28% +571.57% +573.10%] (p = 0.00 < 0.05)
Performance has regressed.
Found 4182 outliers among 100000 measurements (4.18%)
4182 (4.18%) high mild
single_random_f64/Pcg64/sample_inclusive
time: [22.518 ns 22.565 ns 22.611 ns]
change: [+518.80% +520.10% +521.35%] (p = 0.00 < 0.05)
Performance has regressed.
Found 5656 outliers among 100000 measurements (5.66%)
5656 (5.66%) high mild

In comparison, our current implementations benchmark at 1-1.5ns for distribution sampling, ~2ns for single f32 sampling and ~4ns for single f64 sampling on an AMD 5800X at 3.8GHz.

The paper's implementation of SESS benchmarked at 10ns per value for both f32 and f64, while most other methods benchmarked at approx 15ns per value on an Intel 6700HQ at 2.6GHz. It would appear that there is room for optimisation both in the SESS implementation above and in the other implementations as used in the paper, however I am doubtful that the method could perform competitively with our current method.

Note also that #531 should (if I remember correctly) be able to offer more precise sampling and with similar performance, thus I do not see much benefit in considering the γsection method further.

@dhardy

Copy link
Copy Markdown
MemberAuthor

Test failure should be resolved by #1287 (use of feature small_rng for testing benchmarks).

@vks could you review please?

@TheIronBorn

TheIronBorn commented Feb 22, 2023

Copy link
Copy Markdown
Contributor

Are there any significant optimizations we can make for the [0,1) case?

@dhardy

Copy link
Copy Markdown
MemberAuthor

I didn't look at that here, but I don't think so (we essentially just mask and bit-cast an integer, then subtract 1).

Comment threadbenches/uniform_float.rs Outdated

@vksvks left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good! I only have some small questions about the benchmarks.

Comment threadbenches/uniform_float.rs
Comment threadbenches/uniform_float.rs
Comment threadsrc/distributions/uniform.rs Outdated
Also avoid using internal API of Uniform distribution.
vks
vks approved these changes May 1, 2023
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@dhardy@TheIronBorn@vks
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); Uniform float improvements by dhardy · Pull Request #1289 · rust-random/rand · GitHub
Skip to content

Uniform float improvements - #1289

Merged
vks merged 6 commits into
rust-random:masterfrom
dhardy:uniform-float
May 1, 2023
Merged

Uniform float improvements#1289
vks merged 6 commits into
rust-random:masterfrom
dhardy:uniform-float

Conversation

@dhardy

Copy link
Copy Markdown
Member
  • Add a (possibly overcomplicated) benchmark for uniform float sampling
  • Add a couple extra tests
  • Add an explicit impl of sample_single_inclusive (~20% perf increase)

Note: sample_single and sample_single_inclusive use
different code paths for sampling.
Around 20% faster for f32, slightly less for f64.
@dhardy

Copy link
Copy Markdown
MemberAuthor

Closes #1270. This branch was created to evaluate the mentioned paper: https://hal.science/hal-03282794/document

The paper ignores possible underflow, but notes the following potential issues for uniform FP sampling from a defined range:

  • Available precision may vary throughout the given range due to differing exponent
  • The method we use to sample from [0, 1) can only sample half the possible values just below 1
  • Translating from x in [0,1) to a range [a, b) via common methods (such as a + (b-a)*x) can yield value b due to loss of precision. We have special measures for dealing with this.
  • b - a may overflow. (Note: I personally do not think this a big deal since it is rare to use FP values near the limits of their range.)
  • Most transformations do not produce equidistant FP values. In some cases such as Monte Carlo estimation of PI this can impact the result, but in practice I believe it is rarely an issue.

GSL's method, a(1-x) + bx, translates to a(2-z) + b(z-1) where our samples are z in [1, 2) and x = z - 1. This method was benchmarked, with approx 12% performance loss for f32 single-sampling, 2% loss for f64, 10% loss for f32 distribution sampling, and 19% loss for f64. Only 3 of 24 benchmarks showed a performance gain. An attraction of this method is that it avoids the possible overflow of calculating b - a.

The method using y = 2(a/2 + (b/2 - a/2)x) has accuracy issues on sub-normals. In particular, something like f32::from_bits(3) / 2.0 rounds up, thus the result may exceed the upper bound. This is a more significant issue than overflow, hence this method is disqualified from further analysis.

The paper defines method SESS (Same Exponent Same Sign) which we shall ignore due to the narrow circumstances in which it is applicable.

The paper also defines method γsection for general applicability. This is used for sample_single_inclusive, translated to Rust from the Julia snippet γsectionCC as follows:

let(a, b) = (low, high);let g = (a.next_up() - a).max(b - b.next_down());let s = b/g - a/g;let eps = if a.abs() <= b.abs(){
-a/g - (s - b/g)}else{
b /g - (s + a/g)};let si = s.ceil()as $uty;let hi = if s != si as $ty {
si
}else{
si + (eps > 0.0)as $uty
};let k = (0..=hi).sample_single(rng).unwrap();Ok(if a.abs() <= b.abs(){if k == hi {
a
}else{
b - (k as $ty)*g
}}else{if k == hi {
b
}else{
a + (k as $ty)*g
}})

Results are around 10 times slower for f32 and 6 times for f64:

$ cargo bench --bench uniform_float --features small_rng -- -b baseline inclusive
Compiling rand v0.9.0 (/home/dhardy/projects/rand/rand)
Finished bench [optimized] target(s) in 1.47s
Running benches/uniform_float.rs (target/release/deps/uniform_float-177b17dd872a054a)
single_random_f32/SmallRng/sample_inclusive
time: [17.293 ns 17.337 ns 17.377 ns]
change: [+928.42% +930.48% +934.08%] (p = 0.00 < 0.05)
Performance has regressed.
Found 7868 outliers among 100000 measurements (7.87%)
7868 (7.87%) high mild
single_random_f32/ChaCha8Rng/sample_inclusive
time: [18.027 ns 18.072 ns 18.118 ns]
change: [+832.83% +835.21% +837.39%] (p = 0.00 < 0.05)
Performance has regressed.
Found 5987 outliers among 100000 measurements (5.99%)
5987 (5.99%) high mild
single_random_f32/Pcg32/sample_inclusive
time: [17.056 ns 17.096 ns 17.139 ns]
change: [+913.05% +916.00% +918.69%] (p = 0.00 < 0.05)
Performance has regressed.
Found 7752 outliers among 100000 measurements (7.75%)
7752 (7.75%) high mild
single_random_f32/Pcg64/sample_inclusive
time: [17.796 ns 17.838 ns 17.880 ns]
change: [+893.31% +895.96% +898.01%] (p = 0.00 < 0.05)
Performance has regressed.
Found 5205 outliers among 100000 measurements (5.21%)
5205 (5.21%) high mild
single_random_f64/SmallRng/sample_inclusive
time: [22.094 ns 22.143 ns 22.189 ns]
change: [+571.29% +572.87% +574.19%] (p = 0.00 < 0.05)
Performance has regressed.
Found 6735 outliers among 100000 measurements (6.74%)
6735 (6.74%) high mild
single_random_f64/ChaCha8Rng/sample_inclusive
time: [23.074 ns 23.124 ns 23.173 ns]
change: [+445.36% +446.46% +447.50%] (p = 0.00 < 0.05)
Performance has regressed.
Found 3917 outliers among 100000 measurements (3.92%)
3917 (3.92%) high mild
single_random_f64/Pcg32/sample_inclusive
time: [22.403 ns 22.448 ns 22.493 ns]
change: [+570.28% +571.57% +573.10%] (p = 0.00 < 0.05)
Performance has regressed.
Found 4182 outliers among 100000 measurements (4.18%)
4182 (4.18%) high mild
single_random_f64/Pcg64/sample_inclusive
time: [22.518 ns 22.565 ns 22.611 ns]
change: [+518.80% +520.10% +521.35%] (p = 0.00 < 0.05)
Performance has regressed.
Found 5656 outliers among 100000 measurements (5.66%)
5656 (5.66%) high mild

In comparison, our current implementations benchmark at 1-1.5ns for distribution sampling, ~2ns for single f32 sampling and ~4ns for single f64 sampling on an AMD 5800X at 3.8GHz.

The paper's implementation of SESS benchmarked at 10ns per value for both f32 and f64, while most other methods benchmarked at approx 15ns per value on an Intel 6700HQ at 2.6GHz. It would appear that there is room for optimisation both in the SESS implementation above and in the other implementations as used in the paper, however I am doubtful that the method could perform competitively with our current method.

Note also that #531 should (if I remember correctly) be able to offer more precise sampling and with similar performance, thus I do not see much benefit in considering the γsection method further.

@dhardy

Copy link
Copy Markdown
MemberAuthor

Test failure should be resolved by #1287 (use of feature small_rng for testing benchmarks).

@vks could you review please?

@TheIronBorn

TheIronBorn commented Feb 22, 2023

Copy link
Copy Markdown
Contributor

Are there any significant optimizations we can make for the [0,1) case?

@dhardy

Copy link
Copy Markdown
MemberAuthor

I didn't look at that here, but I don't think so (we essentially just mask and bit-cast an integer, then subtract 1).

Comment threadbenches/uniform_float.rs Outdated

@vksvks left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good! I only have some small questions about the benchmarks.

Comment threadbenches/uniform_float.rs
Comment threadbenches/uniform_float.rs
Comment threadsrc/distributions/uniform.rs Outdated
Also avoid using internal API of Uniform distribution.
vks
vks approved these changes May 1, 2023
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@dhardy@TheIronBorn@vks