Skip to content

Commit

Permalink
feat(es/minfiier): Compute more with sequential inliner (#6169)
Browse files Browse the repository at this point in the history
  • Loading branch information
kdy1 committed Oct 27, 2022
1 parent 51132f0 commit 743a1aa
Show file tree
Hide file tree
Showing 44 changed files with 257 additions and 124 deletions.
@@ -1,5 +1,5 @@
//// [compoundAdditionAssignmentLHSCanBeAssigned.ts]
var E, a, b, x1, x2, x3, x4, x6;
var E;
!function(E) {
E[E.a = 0] = "a", E[E.b = 1] = "b";
}(E || (E = {})), x1 += a, x1 += b, x1 += !0, x1 += 0, x1 += "", x1 += E.a, x1 += {}, x1 += null, x1 += void 0, x2 += a, x2 += b, x2 += !0, x2 += 0, x2 += "", x2 += E.a, x2 += {}, x2 += null, x2 += void 0, x3 += a, x3 += 0, x3 += E.a, x3 += null, x3 += void 0, x4 += a, x4 += 0, x4 += E.a, x4 += null, x4 += void 0, x6 += a, x6 += "";
}(E || (E = {})), E.a, E.a, E.a, E.a;
@@ -1,5 +1,5 @@
//// [compoundAdditionAssignmentWithInvalidOperands.ts]
var E, a, x1, x2, x3, x4, x5;
var E;
!function(E) {
E[E.a = 0] = "a", E[E.b = 1] = "b";
}(E || (E = {})), x1 += a, x1 += !0, x1 += 0, x1 += E.a, x1 += {}, x1 += null, x1 += void 0, x2 += a, x2 += !0, x2 += 0, x2 += E.a, x2 += {}, x2 += null, x2 += void 0, x3 += a, x3 += !0, x3 += 0, x3 += E.a, x3 += {}, x3 += null, x3 += void 0, x4 += a, x4 += !0, x4 += {}, x5 += a, x5 += !0, x5 += {};
}(E || (E = {})), E.a, E.a, E.a;
@@ -1,5 +1,5 @@
//// [compoundArithmeticAssignmentLHSCanBeAssigned.ts]
var E, a, b, c, x1, x2, x3;
var E;
!function(E) {
E[E.a = 0] = "a", E[E.b = 1] = "b", E[E.c = 2] = "c";
}(E || (E = {})), x1 *= a, x1 *= b, x1 *= c, x1 *= null, x1 *= void 0, x2 *= a, x2 *= b, x2 *= c, x2 *= null, x2 *= void 0, x3 *= a, x3 *= b, x3 *= c, x3 *= null, x3 *= void 0;
}(E || (E = {}));
@@ -1,5 +1,5 @@
//// [compoundArithmeticAssignmentWithInvalidOperands.ts]
var E, a, b, x1, x2, x3, x4, x5, x6;
var E;
!function(E) {
E[E.a = 0] = "a", E[E.b = 1] = "b";
}(E || (E = {})), x1 *= a, x1 *= b, x1 *= !0, x1 *= 0, x1 *= "", x1 *= E.a, x1 *= {}, x1 *= null, x1 *= void 0, x2 *= a, x2 *= b, x2 *= !0, x2 *= 0, x2 *= "", x2 *= E.a, x2 *= {}, x2 *= null, x2 *= void 0, x3 *= a, x3 *= b, x3 *= !0, x3 *= 0, x3 *= "", x3 *= E.a, x3 *= {}, x3 *= null, x3 *= void 0, x4 *= a, x4 *= b, x4 *= !0, x4 *= 0, x4 *= "", x4 *= E.a, x4 *= {}, x4 *= null, x4 *= void 0, x5 *= b, x5 *= !0, x5 *= "", x5 *= {}, x6 *= b, x6 *= !0, x6 *= "", x6 *= {};
}(E || (E = {})), E.a, E.a, E.a, E.a;
@@ -1,4 +1,4 @@
//// [destructuringControlFlow.ts]
(0, [
[
"foo"
][1]).toUpperCase();
][1].toUpperCase();
2 changes: 1 addition & 1 deletion crates/swc/tests/tsc-references/symbolType12.2.minified.js
@@ -1,3 +1,3 @@
//// [symbolType12.ts]
var s = Symbol.for("assign"), str = "";
s *= s, s *= 0, s /= s, s /= 0, s %= s, s %= 0, s += s, s += 0, s += "", str += s, s -= s, s -= 0, s <<= s, s <<= 0, s >>= s, s >>= 0, s >>>= s, s >>>= 0, s &= s, s &= 0, s ^= s, s ^= 0, s |= s, s |= 0, str += s || str;
s *= 0, s /= s, s /= 0, s %= s, s %= 0, str += s = s + 0 + "", s -= s, s -= 0, s <<= s, s <<= 0, s >>= s, s >>= 0, s >>>= s, s >>>= 0, s &= s, s &= 0, s ^= s, s ^= 0, s |= s, s |= 0, str += s || str;
26 changes: 26 additions & 0 deletions crates/swc_ecma_minifier/src/compress/optimize/inline.rs
@@ -1,7 +1,9 @@
use swc_atoms::js_word;
use swc_common::{util::take::Take, EqIgnoreSpan, Spanned};
use swc_ecma_ast::*;
use swc_ecma_transforms_optimization::simplify::expr_simplifier;
use swc_ecma_utils::{class_has_side_effect, find_pat_ids, ExprExt};
use swc_ecma_visit::VisitMutWith;

use super::Optimizer;
use crate::{
Expand Down Expand Up @@ -680,6 +682,30 @@ where

/// Actually inlines variables.
pub(super) fn inline(&mut self, e: &mut Expr) {
if let Expr::Member(me) = e {
if let MemberProp::Computed(ref mut prop) = me.prop {
if let Expr::Lit(Lit::Num(..)) = &*prop.expr {
if let Expr::Ident(obj) = &*me.obj {
let new = self.vars.lits_for_array_access.get(&obj.to_id());

if let Some(new) = new {
report_change!("inline: Inlined array access");
self.changed = true;

me.obj = new.clone();
// TODO(kdy1): Optimize performance by skipping visiting of children
// nodes.
e.visit_mut_with(&mut expr_simplifier(
self.marks.unresolved_mark,
Default::default(),
));
}
return;
}
}
}
}

if let Expr::Ident(i) = e {
let id = i.to_id();
if let Some(value) = self
Expand Down
92 changes: 70 additions & 22 deletions crates/swc_ecma_minifier/src/compress/optimize/sequences.rs
Expand Up @@ -1665,6 +1665,20 @@ where
};

if !self.is_skippable_for_seq(Some(a), &Expr::Ident(b_left.clone())) {
// Let's be safe
if IdentUsageFinder::find(&b_left.to_id(), &b_assign.right) {
return Ok(false);
}

// As we are not *skipping* lhs, we can inline here
if let Some(a_id) = a.id() {
if a_id == b_left.to_id() {
if self.replace_seq_assignment(a, b)? {
return Ok(true);
}
}
}

return Ok(false);
}

Expand Down Expand Up @@ -2202,7 +2216,7 @@ where
}

macro_rules! take_a {
($force_drop:expr) => {
($force_drop:expr, $drop_op:expr) => {
match a {
Mergable::Var(a) => {
if self.options.unused {
Expand All @@ -2224,15 +2238,17 @@ where
.unwrap_or_else(|| undefined(DUMMY_SP))
}
Mergable::Expr(a) => {
if can_remove {
if can_remove || $force_drop {
if let Expr::Assign(e) = a {
report_change!(
"sequences: Dropping assignment as we are going to drop the \
variable declaration. ({})",
left_id
);

**a = *e.right.take();
if e.op == op!("=") || $drop_op {
report_change!(
"sequences: Dropping assignment as we are going to drop \
the variable declaration. ({})",
left_id
);

**a = *e.right.take();
}
}
}

Expand All @@ -2256,7 +2272,10 @@ where
Expr::Assign(b @ AssignExpr { op: op!("="), .. }) => {
if let Some(b_left) = b.left.as_ident() {
if b_left.to_id() == left_id.to_id() {
let mut a_expr = take_a!(true);
report_change!("sequences: Merged assignment into another assignment");
self.changed = true;

let mut a_expr = take_a!(true, false);
let a_expr = self.ignore_return_value(&mut a_expr);

if let Some(a) = a_expr {
Expand All @@ -2271,19 +2290,35 @@ where
}
Expr::Assign(b) => {
if let Some(b_left) = b.left.as_ident() {
if b_left.to_id() == left_id.to_id() {
if let Some(bin_op) = b.op.to_update() {
b.op = op!("=");
let a_op = match a {
Mergable::Var(_) => Some(op!("=")),
Mergable::Expr(Expr::Assign(AssignExpr { op: a_op, .. })) => Some(*a_op),
Mergable::FnDecl(_) => Some(op!("=")),
_ => None,
};

let to = take_a!(true);
if let Some(a_op) = a_op {
if can_drop_op_for(a_op, b.op) {
if b_left.to_id() == left_id.to_id() {
if let Some(bin_op) = b.op.to_update() {
report_change!(
"sequences: Merged assignment into another (op) assignment"
);
self.changed = true;

b.right = Box::new(Expr::Bin(BinExpr {
span: DUMMY_SP,
op: bin_op,
left: to,
right: b.right.take(),
}));
return Ok(true);
b.op = op!("=");

let to = take_a!(true, true);

b.right = Box::new(Expr::Bin(BinExpr {
span: DUMMY_SP,
op: bin_op,
left: to,
right: b.right.take(),
}));
return Ok(true);
}
}
}
}
}
Expand Down Expand Up @@ -2319,7 +2354,7 @@ where
left_id.span.ctxt
);

let to = take_a!(false);
let to = take_a!(false, false);

replace_id_with_expr(b, left_id.to_id(), to);

Expand Down Expand Up @@ -2449,3 +2484,16 @@ pub(crate) fn is_trivial_lit(e: &Expr) -> bool {
_ => false,
}
}

/// This assumes `a.left.to_id() == b.left.to_id()`
fn can_drop_op_for(a: AssignOp, b: AssignOp) -> bool {
if a == op!("=") {
return true;
}

if a == b {
return matches!(a, op!("+=") | op!("*="));
}

false
}
12 changes: 6 additions & 6 deletions crates/swc_ecma_minifier/tests/benches-full/echarts.js
Expand Up @@ -5298,7 +5298,7 @@
var width = data[i++], height = data[i++];
if (x1 = x0 + width, y1 = y0 + height, isStroke) {
if (containStroke(x0, y0, x1, y0, lineWidth, x, y) || containStroke(x1, y0, x1, y1, lineWidth, x, y) || containStroke(x1, y1, x0, y1, lineWidth, x, y) || containStroke(x0, y1, x0, y0, lineWidth, x, y)) return !0;
} else w += windingLine(x1, y0, x1, y1, x, y), w += windingLine(x0, y1, x0, y0, x, y);
} else w = windingLine(x1, y0, x1, y1, x, y) + windingLine(x0, y1, x0, y0, x, y);
break;
case CMD$1.Z:
if (isStroke) {
Expand Down Expand Up @@ -12349,7 +12349,7 @@
function decodePolygon(coordinate, encodeOffsets, encodeScale) {
for(var result = [], prevX = encodeOffsets[0], prevY = encodeOffsets[1], i = 0; i < coordinate.length; i += 2){
var x = coordinate.charCodeAt(i) - 64, y = coordinate.charCodeAt(i + 1) - 64;
x = x >> 1 ^ -(1 & x), y = y >> 1 ^ -(1 & y), x += prevX, y += prevY, prevX = x, prevY = y, result.push([
y = y >> 1 ^ -(1 & y), x = (x >> 1 ^ -(1 & x)) + prevX, y += prevY, prevX = x, prevY = y, result.push([
x / encodeScale,
y / encodeScale
]);
Expand Down Expand Up @@ -15423,17 +15423,17 @@
case 'half-year':
case 'quarter':
case 'month':
approxInterval1 = approxInterval, interval = (approxInterval1 /= 2592000000) > 6 ? 6 : approxInterval1 > 3 ? 3 : approxInterval1 > 2 ? 2 : 1, getterName = monthGetterName(isUTC), setterName = monthSetterName(isUTC);
interval = (approxInterval1 = approxInterval / 2592000000) > 6 ? 6 : approxInterval1 > 3 ? 3 : approxInterval1 > 2 ? 2 : 1, getterName = monthGetterName(isUTC), setterName = monthSetterName(isUTC);
break;
case 'week':
case 'half-week':
case 'day':
approxInterval2 = approxInterval, interval = (approxInterval2 /= 86400000) > 16 ? 16 : approxInterval2 > 7.5 ? 7 : approxInterval2 > 3.5 ? 4 : approxInterval2 > 1.5 ? 2 : 1, getterName = dateGetterName(isUTC), setterName = dateSetterName(isUTC);
interval = (approxInterval2 = approxInterval / 86400000) > 16 ? 16 : approxInterval2 > 7.5 ? 7 : approxInterval2 > 3.5 ? 4 : approxInterval2 > 1.5 ? 2 : 1, getterName = dateGetterName(isUTC), setterName = dateSetterName(isUTC);
break;
case 'half-day':
case 'quarter-day':
case 'hour':
approxInterval3 = approxInterval, interval = (approxInterval3 /= 3600000) > 12 ? 12 : approxInterval3 > 6 ? 6 : approxInterval3 > 3.5 ? 4 : approxInterval3 > 2 ? 2 : 1, getterName = hoursGetterName(isUTC), setterName = hoursSetterName(isUTC);
interval = (approxInterval3 = approxInterval / 3600000) > 12 ? 12 : approxInterval3 > 6 ? 6 : approxInterval3 > 3.5 ? 4 : approxInterval3 > 2 ? 2 : 1, getterName = hoursGetterName(isUTC), setterName = hoursSetterName(isUTC);
break;
case 'minute':
interval = getMinutesAndSecondsInterval(approxInterval, !0), getterName = minutesGetterName(isUTC), setterName = minutesSetterName(isUTC);
Expand Down Expand Up @@ -34777,7 +34777,7 @@
var blockMetaList = result1.meta, buttonContainer = document.createElement('div');
buttonContainer.style.cssText = 'position:absolute;bottom:0;left:0;right:0;';
var buttonStyle = "float:right;margin-right:20px;border:none;cursor:pointer;padding:2px 5px;font-size:12px;border-radius:3px", closeButton = document.createElement('div'), refreshButton = document.createElement('div');
buttonStyle += ';background-color:' + model.get('buttonColor'), buttonStyle += ';color:' + model.get('buttonTextColor');
buttonStyle = ';background-color:' + model.get('buttonColor') + ';color:' + model.get('buttonTextColor');
var self1 = this;
function close() {
container.removeChild(root), self1._dom = null;
Expand Down
2 changes: 1 addition & 1 deletion crates/swc_ecma_minifier/tests/benches-full/moment.js
Expand Up @@ -799,7 +799,7 @@
getParsingFlags(config).invalidFormat = !0, config._d = new Date(NaN);
return;
}
for(i = 0; i < config._f.length; i++)currentScore = 0, validFormatFound = !1, tempConfig = copyConfig({}, config), null != config._useUTC && (tempConfig._useUTC = config._useUTC), tempConfig._f = config._f[i], configFromStringAndFormat(tempConfig), isValid(tempConfig) && (validFormatFound = !0), currentScore += getParsingFlags(tempConfig).charsLeftOver, currentScore += 10 * getParsingFlags(tempConfig).unusedTokens.length, getParsingFlags(tempConfig).score = currentScore, bestFormatIsValid ? currentScore < scoreToBeat && (scoreToBeat = currentScore, bestMoment = tempConfig) : (null == scoreToBeat || currentScore < scoreToBeat || validFormatFound) && (scoreToBeat = currentScore, bestMoment = tempConfig, validFormatFound && (bestFormatIsValid = !0));
for(i = 0; i < config._f.length; i++)currentScore = 0, validFormatFound = !1, tempConfig = copyConfig({}, config), null != config._useUTC && (tempConfig._useUTC = config._useUTC), tempConfig._f = config._f[i], configFromStringAndFormat(tempConfig), isValid(tempConfig) && (validFormatFound = !0), currentScore = getParsingFlags(tempConfig).charsLeftOver + 10 * getParsingFlags(tempConfig).unusedTokens.length, getParsingFlags(tempConfig).score = currentScore, bestFormatIsValid ? currentScore < scoreToBeat && (scoreToBeat = currentScore, bestMoment = tempConfig) : (null == scoreToBeat || currentScore < scoreToBeat || validFormatFound) && (scoreToBeat = currentScore, bestMoment = tempConfig, validFormatFound && (bestFormatIsValid = !0));
extend(config, bestMoment || tempConfig);
}(config) : format ? configFromStringAndFormat(config) : isUndefined(input = (config1 = config)._i) ? config1._d = new Date(hooks.now()) : isDate(input) ? config1._d = new Date(input.valueOf()) : 'string' == typeof input ? function(config) {
var matched = aspNetJsonRegex.exec(config._i);
Expand Down
4 changes: 2 additions & 2 deletions crates/swc_ecma_minifier/tests/benches-full/terser.js
Expand Up @@ -1912,8 +1912,8 @@
base && base.PROPS && (props = props.concat(base.PROPS));
for(var code = "return function AST_" + type + "(props){ if (props) { ", i = props.length; --i >= 0;)code += "this." + props[i] + " = props." + props[i] + ";";
const proto = base && Object.create(base.prototype);
(proto && proto.initialize || methods && methods.initialize) && (code += "this.initialize();"), code += "}", code += "this.flags = 0;";
var ctor = Function(code += "}")();
(proto && proto.initialize || methods && methods.initialize) && (code += "this.initialize();");
var ctor = Function(code = "}this.flags = 0;}")();
if (proto && (ctor.prototype = proto, ctor.BASE = base), base && base.SUBCLASSES.push(ctor), ctor.prototype.CTOR = ctor, ctor.prototype.constructor = ctor, ctor.PROPS = props || null, ctor.SELF_PROPS = self_props, ctor.SUBCLASSES = [], type && (ctor.prototype.TYPE = ctor.TYPE = type), methods) for(i in methods)HOP(methods, i) && ("$" === i[0] ? ctor[i.substr(1)] = methods[i] : ctor.prototype[i] = methods[i]);
return ctor.DEFMETHOD = function(name, method) {
this.prototype[name] = method;
Expand Down
Expand Up @@ -6035,7 +6035,7 @@
if (number != number) return "NaN";
if (number <= -1000000000000000000000 || number >= 1e21) return String(number);
if (number < 0 && (sign = "-", number = -number), number > 1e-21) {
if (z = (e = log(number * pow(2, 69, 1)) - 69) < 0 ? number * pow(2, -e, 1) : number / pow(2, e, 1), z *= 0x10000000000000, (e = 52 - e) > 0) {
if (z = ((e = log(number * pow(2, 69, 1)) - 69) < 0 ? number * pow(2, -e, 1) : number / pow(2, e, 1)) * 0x10000000000000, (e = 52 - e) > 0) {
for(multiply(data, 0, z), j = fractDigits; j >= 7;)multiply(data, 1e7, 0), j -= 7;
for(multiply(data, pow(10, j, 1), 0), j = e - 1; j >= 23;)divide(data, 8388608), j -= 23;
divide(data, 1 << j), multiply(data, 1, 1), divide(data, 2), result = dataToString(data);
Expand Down Expand Up @@ -16028,7 +16028,7 @@
}, _proto.componentWillReceiveProps = function(nextProps) {
if (this.props.value !== nextProps.value) {
var changedBits, oldValue = this.props.value, newValue = nextProps.value;
(oldValue === newValue ? 0 !== oldValue || 1 / oldValue == 1 / newValue : oldValue != oldValue && newValue != newValue) ? changedBits = 0 : (changedBits = "function" == typeof calculateChangedBits ? calculateChangedBits(oldValue, newValue) : 1073741823, 0 != (changedBits |= 0) && this.emitter.set(nextProps.value, changedBits));
(oldValue === newValue ? 0 !== oldValue || 1 / oldValue == 1 / newValue : oldValue != oldValue && newValue != newValue) ? changedBits = 0 : 0 != (changedBits = ("function" == typeof calculateChangedBits ? calculateChangedBits(oldValue, newValue) : 1073741823) | 0) && this.emitter.set(nextProps.value, changedBits);
}
}, _proto.render = function() {
return this.props.children;
Expand Down
Expand Up @@ -1287,7 +1287,7 @@
d = !0;
}
d || (b1 = "", xa(c, function(c, d) {
b1 += d, b1 += ":", b1 += c, b1 += "\r\n";
b1 = d + ":" + c + "\r\n";
}), c = b1, "string" == typeof a ? null != c && encodeURIComponent(String(c)) : R(a, b, c));
}
function Hd(a, b, c) {
Expand Down

1 comment on commit 743a1aa

@github-actions
Copy link

Choose a reason for hiding this comment

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

Benchmark

Benchmark suite Current: 743a1aa Previous: 782da5c Ratio
es/full/bugs-1 338859 ns/iter (± 19165) 372969 ns/iter (± 197640) 0.91
es/full/minify/libraries/antd 1807361757 ns/iter (± 42942553) 1906719838 ns/iter (± 60376461) 0.95
es/full/minify/libraries/d3 383691809 ns/iter (± 12991801) 432136029 ns/iter (± 25410991) 0.89
es/full/minify/libraries/echarts 1541896169 ns/iter (± 24128297) 1617301679 ns/iter (± 41457861) 0.95
es/full/minify/libraries/jquery 97437574 ns/iter (± 3473725) 115021948 ns/iter (± 4587068) 0.85
es/full/minify/libraries/lodash 114406438 ns/iter (± 1436726) 132318824 ns/iter (± 6858028) 0.86
es/full/minify/libraries/moment 57999540 ns/iter (± 990658) 69577365 ns/iter (± 6560088) 0.83
es/full/minify/libraries/react 19392601 ns/iter (± 478067) 22625404 ns/iter (± 917119) 0.86
es/full/minify/libraries/terser 357911001 ns/iter (± 143841951) 355190095 ns/iter (± 21482023) 1.01
es/full/minify/libraries/three 538254061 ns/iter (± 10344689) 582006848 ns/iter (± 62832958) 0.92
es/full/minify/libraries/typescript 3398886400 ns/iter (± 77681435) 3613809306 ns/iter (± 66494279) 0.94
es/full/minify/libraries/victory 814345267 ns/iter (± 37635308) 862079462 ns/iter (± 42409133) 0.94
es/full/minify/libraries/vue 145270534 ns/iter (± 4043914) 172266290 ns/iter (± 23209983) 0.84
es/full/codegen/es3 33633 ns/iter (± 1450) 32783 ns/iter (± 422) 1.03
es/full/codegen/es5 33333 ns/iter (± 3277) 32785 ns/iter (± 835) 1.02
es/full/codegen/es2015 33311 ns/iter (± 727) 32730 ns/iter (± 945) 1.02
es/full/codegen/es2016 33232 ns/iter (± 914) 32702 ns/iter (± 1034) 1.02
es/full/codegen/es2017 33261 ns/iter (± 1295) 32733 ns/iter (± 637) 1.02
es/full/codegen/es2018 33253 ns/iter (± 675) 32625 ns/iter (± 623) 1.02
es/full/codegen/es2019 33295 ns/iter (± 583) 32814 ns/iter (± 603) 1.01
es/full/codegen/es2020 33264 ns/iter (± 974) 32676 ns/iter (± 750) 1.02
es/full/all/es3 185946772 ns/iter (± 10381718) 212479827 ns/iter (± 11982743) 0.88
es/full/all/es5 176159583 ns/iter (± 2767116) 202692639 ns/iter (± 13238644) 0.87
es/full/all/es2015 141727264 ns/iter (± 3737441) 160011448 ns/iter (± 11551966) 0.89
es/full/all/es2016 140608961 ns/iter (± 4037461) 159959199 ns/iter (± 9424651) 0.88
es/full/all/es2017 140009277 ns/iter (± 2781809) 157933475 ns/iter (± 9065317) 0.89
es/full/all/es2018 138744681 ns/iter (± 4257324) 158479888 ns/iter (± 10422918) 0.88
es/full/all/es2019 137928365 ns/iter (± 3423663) 148375646 ns/iter (± 8929590) 0.93
es/full/all/es2020 132987276 ns/iter (± 3115723) 150443719 ns/iter (± 9757128) 0.88
es/full/parser 695014 ns/iter (± 64121) 734967 ns/iter (± 22701) 0.95
es/full/base/fixer 24891 ns/iter (± 1019) 26141 ns/iter (± 1232) 0.95
es/full/base/resolver_and_hygiene 87499 ns/iter (± 1637) 92924 ns/iter (± 3599) 0.94
serialization of ast node 208 ns/iter (± 4) 214 ns/iter (± 8) 0.97
serialization of serde 211 ns/iter (± 10) 220 ns/iter (± 8) 0.96

This comment was automatically generated by workflow using github-action-benchmark.

Please sign in to comment.