nushell/crates/nu-command/src/math/reducers.rs
Stefan Holderbach a52386e837
Box ShellError in Value::Error (#8375)
# Description

Our `ShellError` at the moment has a `std::mem::size_of<ShellError>` of
136 bytes (on AMD64). As a result `Value` directly storing the struct
also required 136 bytes (thanks to alignment requirements).

This change stores the `Value::Error` `ShellError` on the heap.

Pro:
- Value now needs just 80 bytes
- Should be 1 cacheline less (still at least 2 cachelines)

Con:
- More small heap allocations when dealing with `Value::Error`
  - More heap fragmentation
  - Potential for additional required memcopies

# Further code changes

Includes a small refactor of `try` due to a type mismatch in its large
match.

# User-Facing Changes

None for regular users.

Plugin authors may have to update their matches on `Value` if they use
`nu-protocol`

Needs benchmarking to see if there is a benefit in real world workloads.
**Update** small improvements in runtime for workloads with high volume
of values. Significant reduction in maximum resident set size, when many
values are held in memory.

# Tests + Formatting
2023-03-12 09:57:27 +01:00

162 lines
4.9 KiB
Rust

use nu_protocol::{ShellError, Span, Value};
use std::cmp::Ordering;
pub enum Reduce {
Summation,
Product,
Minimum,
Maximum,
}
pub type ReducerFunction =
Box<dyn Fn(Value, Vec<Value>, Span, Span) -> Result<Value, ShellError> + Send + Sync + 'static>;
pub fn reducer_for(command: Reduce) -> ReducerFunction {
match command {
Reduce::Summation => Box::new(|_, values, span, head| sum(values, span, head)),
Reduce::Product => Box::new(|_, values, span, head| product(values, span, head)),
Reduce::Minimum => Box::new(|_, values, span, head| min(values, span, head)),
Reduce::Maximum => Box::new(|_, values, span, head| max(values, span, head)),
}
}
pub fn max(data: Vec<Value>, span: Span, head: Span) -> Result<Value, ShellError> {
let mut biggest = data
.first()
.ok_or_else(|| {
ShellError::UnsupportedInput(
"Empty input".to_string(),
"value originates from here".into(),
head,
span,
)
})?
.clone();
for value in &data {
if let Some(result) = value.partial_cmp(&biggest) {
if result == Ordering::Greater {
biggest = value.clone();
}
} else {
return Err(ShellError::OperatorMismatch {
op_span: head,
lhs_ty: biggest.get_type(),
lhs_span: biggest.span()?,
rhs_ty: value.get_type(),
rhs_span: value.span()?,
});
}
}
Ok(biggest)
}
pub fn min(data: Vec<Value>, span: Span, head: Span) -> Result<Value, ShellError> {
let mut smallest = data
.first()
.ok_or_else(|| {
ShellError::UnsupportedInput(
"Empty input".to_string(),
"value originates from here".into(),
head,
span,
)
})?
.clone();
for value in &data {
if let Some(result) = value.partial_cmp(&smallest) {
if result == Ordering::Less {
smallest = value.clone();
}
} else {
return Err(ShellError::OperatorMismatch {
op_span: head,
lhs_ty: smallest.get_type(),
lhs_span: smallest.span()?,
rhs_ty: value.get_type(),
rhs_span: value.span()?,
});
}
}
Ok(smallest)
}
pub fn sum(data: Vec<Value>, span: Span, head: Span) -> Result<Value, ShellError> {
let initial_value = data.get(0);
let mut acc = match initial_value {
Some(Value::Filesize { span, .. }) => Ok(Value::Filesize {
val: 0,
span: *span,
}),
Some(Value::Duration { span, .. }) => Ok(Value::Duration {
val: 0,
span: *span,
}),
Some(Value::Int { span, .. }) | Some(Value::Float { span, .. }) => Ok(Value::int(0, *span)),
None => Err(ShellError::UnsupportedInput(
"Empty input".to_string(),
"value originates from here".into(),
head,
span,
)),
_ => Ok(Value::nothing(head)),
}?;
for value in &data {
match value {
Value::Int { .. }
| Value::Float { .. }
| Value::Filesize { .. }
| Value::Duration { .. } => {
acc = acc.add(head, value, head)?;
}
Value::Error { error } => return Err(*error.clone()),
other => {
return Err(ShellError::UnsupportedInput(
"Attempted to compute the sum of a value that cannot be summed".to_string(),
"value originates from here".into(),
head,
other.expect_span(),
));
}
}
}
Ok(acc)
}
pub fn product(data: Vec<Value>, span: Span, head: Span) -> Result<Value, ShellError> {
let initial_value = data.get(0);
let mut acc = match initial_value {
Some(Value::Int { span, .. }) | Some(Value::Float { span, .. }) => Ok(Value::int(1, *span)),
None => Err(ShellError::UnsupportedInput(
"Empty input".to_string(),
"value originates from here".into(),
head,
span,
)),
_ => Ok(Value::nothing(head)),
}?;
for value in &data {
match value {
Value::Int { .. } | Value::Float { .. } => {
acc = acc.mul(head, value, head)?;
}
Value::Error { error } => return Err(*error.clone()),
other => {
return Err(ShellError::UnsupportedInput(
"Attempted to compute the product of a value that cannot be multiplied"
.to_string(),
"value originates from here".into(),
head,
other.expect_span(),
));
}
}
}
Ok(acc)
}