Merge pull request #3092 from jtracey/join-performance

join: improve performance
This commit is contained in:
Sylvestre Ledru
2022-02-10 22:39:12 +01:00
committed by GitHub
5 changed files with 333 additions and 107 deletions
Generated
+1
View File
@@ -2444,6 +2444,7 @@ name = "uu_join"
version = "0.0.12"
dependencies = [
"clap 3.0.10",
"memchr 2.4.1",
"uucore",
]
+55
View File
@@ -0,0 +1,55 @@
# Benchmarking join
<!-- spell-checker:ignore (words) CSVs nocheck hotpaths -->
## Performance profile
The amount of time spent in which part of the code can vary depending on the files being joined and the flags used.
A benchmark with `-j` and `-i` shows the following time:
| Function/Method | Fraction of Samples | Why? |
| ---------------- | ------------------- | ---- |
| `Line::new` | 27% | Linear search for field separators, plus some vector operations. |
| `read_until` | 22% | Mostly libc reading file contents, with a few vector operations to represent them. |
| `Input::compare` | 20% | ~2/3 making the keys lowercase, ~1/3 comparing them. |
| `print_fields` | 11% | Writing to and flushing the buffer. |
| Other | 20% | |
| libc | 25% | I/O and memory allocation. |
More detailed profiles can be obtained via [flame graphs](https://github.com/flamegraph-rs/flamegraph):
```
cargo flamegraph --bin join --package uu_join -- file1 file2 > /dev/null
```
You may need to add the following lines to the top-level `Cargo.toml` to get full stack traces:
```
[profile.release]
debug = true
```
## How to benchmark
Benchmarking typically requires files large enough to ensure that the benchmark is not overwhelmed by background system noise; say, on the order of tens of MB.
While `join` operates on line-oriented data, and not properly formatted CSVs (e.g., `join` is not designed to accommodate escaped or quoted delimiters),
in practice many CSV datasets will function well after being sorted.
Like most of the utils, the recommended tool for benchmarking is [hyperfine](https://github.com/sharkdp/hyperfine).
To benchmark your changes:
- checkout the main branch (without your changes), do a `--release` build, and back up the executable produced at `target/release/join`
- checkout your working branch (with your changes), do a `--release` build
- run
```
hyperfine -w 5 "/path/to/main/branch/build/join file1 file2" "/path/to/working/branch/build/join file1 file2"
```
- you'll likely need to add additional options to both commands, such as a field separator, or if you're benchmarking some particular behavior
- you can also optionally benchmark against GNU's join
## What to benchmark
The following options can have a non-trivial impact on performance:
- `-a`/`-v` if one of the two files has significantly more lines than the other
- `-j`/`-1`/`-2` cause work to be done to grab the appropriate field
- `-i` adds a call to `to_ascii_lowercase()` that adds some time for allocating and dropping memory for the lowercase key
- `--nocheck-order` causes some calls of `Input::compare` to be skipped
The content of the files being joined has a very significant impact on the performance.
Things like how long each line is, how many fields there are, how long the key fields are, how many lines there are, how many lines can be joined, and how many lines each line can be joined with all change the behavior of the hotpaths.
+1
View File
@@ -17,6 +17,7 @@ path = "src/join.rs"
[dependencies]
clap = { version = "3.0", features = ["wrap_help", "cargo"] }
uucore = { version=">=0.0.11", package="uucore", path="../../uucore" }
memchr = "2"
[[bin]]
name = "join"
+223 -102
View File
File diff suppressed because it is too large Load Diff
+53 -5
View File
@@ -1,6 +1,8 @@
// spell-checker:ignore (words) autoformat
// spell-checker:ignore (words) autoformat nocheck
use crate::common::util::*;
#[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "netbsd"))]
use std::fs::OpenOptions;
#[cfg(unix)]
use std::{ffi::OsStr, os::unix::ffi::OsStrExt};
#[cfg(windows)]
@@ -306,6 +308,16 @@ fn missing_format_fields() {
.stdout_only_fixture("missing_format_fields.expected");
}
#[test]
fn nocheck_order() {
new_ucmd!()
.arg("fields_1.txt")
.arg("fields_2.txt")
.arg("--nocheck-order")
.succeeds()
.stdout_only_fixture("default.expected");
}
#[test]
fn wrong_line_order() {
let ts = TestScenario::new(util_name!());
@@ -313,11 +325,23 @@ fn wrong_line_order() {
.arg("fields_2.txt")
.arg("fields_4.txt")
.fails()
.stdout_contains("7 g f 4 fg")
.stderr_is(&format!(
"{0} {1}: fields_4.txt:5: is not sorted: 11 g 5 gh\n{0} {1}: input is not in sorted order",
ts.bin_path.to_string_lossy(),
ts.util_name
));
"{0} {1}: fields_4.txt:5: is not sorted: 11 g 5 gh\n{0} {1}: input is not in sorted order",
ts.bin_path.to_string_lossy(),
ts.util_name
));
new_ucmd!()
.arg("--check-order")
.arg("fields_2.txt")
.arg("fields_4.txt")
.fails()
.stdout_does_not_contain("7 g f 4 fg")
.stderr_is(&format!(
"{0}: fields_4.txt:5: is not sorted: 11 g 5 gh",
ts.util_name
));
}
#[test]
@@ -327,11 +351,23 @@ fn both_files_wrong_line_order() {
.arg("fields_4.txt")
.arg("fields_5.txt")
.fails()
.stdout_contains("5 e 3 ef")
.stderr_is(&format!(
"{0} {1}: fields_5.txt:4: is not sorted: 3\n{0} {1}: fields_4.txt:5: is not sorted: 11 g 5 gh\n{0} {1}: input is not in sorted order",
ts.bin_path.to_string_lossy(),
ts.util_name
));
new_ucmd!()
.arg("--check-order")
.arg("fields_4.txt")
.arg("fields_5.txt")
.fails()
.stdout_does_not_contain("5 e 3 ef")
.stderr_is(&format!(
"{0}: fields_5.txt:4: is not sorted: 3",
ts.util_name
));
}
#[test]
@@ -437,3 +473,15 @@ fn null_line_endings() {
.succeeds()
.stdout_only_fixture("z.expected");
}
#[test]
#[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "netbsd"))]
fn test_full() {
let dev_full = OpenOptions::new().write(true).open("/dev/full").unwrap();
new_ucmd!()
.arg("fields_1.txt")
.arg("fields_2.txt")
.set_stdout(dev_full)
.fails()
.stderr_contains("No space left on device");
}