Non Negligible Cost of $() spawn.
Calling $()
to capture output carries a non negligible cost in heavy bash environments.
In my bash environment (albeit its a heavy one with lots of functions), the cost is quite non negligible for calling a function directly versus spawning a child shell with $()
and capturing the output.
main() {
echo "now_millis"
# real 0m0.000s
# user 0m0.000s
# sys 0m0.000s
time now_millis
echo "--------------------------------------------------------------------------------"
# real 0m0.008s
# user 0m0.007s
# sys 0m0.001s
time echo "$(now_millis)"
}
main
Result pattern
Result pattern
Use name ref pattern instead
While RESULT pattern could be useful for older BASH. If you have access to modern bash name reference pattern is MUCH preferred as it doesn't have the gotchas of result pattern. While retaining similar performance characteristics (RESULT pattern is faster than name ref though see Result pattern is Faster than Name Ref pattern.).
See: Name Reference Functions for Performance Improvement
Result Pattern
To address the cost of spawning a process to capture output $()
RESULT pattern can be used where we set a variable as means of communicating back results from the function rather than printing to STDOUT and capturing it with $()
, so that we avoid spawning a child process with $()
Example problem
Let's say you have a function that is called in a performant way like now_millis
You have optimized the function itself using EPOCHREALTIME
to avoid function calls within it when possible, and the function runs
now_millis(){
if [[ -n "${EPOCHREALTIME}" ]]; then
# Using EPOCHREALTIME (Bash 5.0+) - faster than 'date' (date uses a couple millis)
# This uses a bash internal variable, no external process needed
#
# EPOCHREALTIME gives seconds.microseconds
# Convert to milliseconds by removing the decimal and dividing by 1000
local epoch_micro="${EPOCHREALTIME//.}"
echo "$((epoch_micro / 1000))"
else
# Fallback for older bash versions
gnu_date +%s%3N || throw "now_millis: Could not get the milliseconds using [gnu_date +%s%3N]"
fi
}
export -f now_millis
And the function runs FAST enough to not be detectable by time
❯time now_millis
1758923999550
real 0m0.000s
user 0m0.000s
sys 0m0.000s
But the problem is that to use it you have to spawn a subshell (to capture current now_millis). Now its quite detectable by time
.
❯time echo "$(now_millis)"
1758924102082
real 0m0.003s
user 0m0.000s
sys 0m0.003s
Result pattern solution:
# Sets _NOW_MILLIS variable upon successful execution.
#
# The use case for this over $(now_millis) is to avoid spawning a child process.
set_NOW_MILLIS() {
if [[ -n "${EPOCHREALTIME}" ]]; then
local epoch_micro="${EPOCHREALTIME//.}"
_NOW_MILLIS="$((epoch_micro / 1000))"
else
_NOW_MILLIS="$(gnu_date +%s%3N)" || throw "set_NOW_MILLIS: Could not get the milliseconds using [gnu_date +%s%3N]"
fi
}
export -f set_NOW_MILLIS
Now we can call the function and get results back WITHOUT spawning a child process.
set_NOW_MILLIS
local start_millis="${_NOW_MILLIS:?}"
Safer Result pattern: unset after read.
The main caveat is not making a mistake of referencing _NOW_MILLIS
without calling set_NOW_MILLIS
each time. As _NOW_MILLIS will contain the value from the last set_NOW_MILLIS
.
We can also unset
the variable after the get to make this pattern safer:
set_NOW_MILLIS
local start_millis="${_NOW_MILLIS:?}"
unset _NOW_MILLIS
After unset
any attempts to use ${_NOW_MILLIS:?}
with :?
check will interrupt the execution.
Name ref pattern
Problem
Non Negligible Cost of $() spawn. when we want to call function and capture its output to a variable.
Solution
Name reference pattern gives us ability to deal with Non Negligible Cost of $() spawn. in quite elegant way. By allowing to pass in a reference variable to a function, letting that function modify that variable. Think of it as passing an object reference in programming languages.
Syntax
local -n _out_some_name="${1:?name of external variable}"
Simple example
foo() {
local -n _out_some_name="${1:?name of external variable}"
_out_some_name=42
}
export -f foo
main() {
local main_var
# We pass a name reference of main_var to foo for foo to modify.
foo main_var
# Now we can get 42 without having to spawn a child shell $(foo)
echo "${main_var:?}"
}
main "${@}" || exit 1
Real Example:
Real example
# Sets a variable to the current time in milliseconds without spawning a subshell.
#
# This function uses bash nameref to directly set the caller's variable, avoiding
# the performance overhead of command substitution $().
#
# Usage:
# put__now_millis <var_name>
#
# Arguments:
# var_name - Name of the variable to set with the current time in milliseconds
#
# Example:
# ```
# # local usage is optional but recommended for scope, otherwise variables will be global
# local start_time end_time
# put__now_millis start_time
# do_something
# put__now_millis end_time
# echo "Operation took $((end_time - start_time))ms"
# ```
#
# Note: Requires Bash 4.3+ for nameref support
# Note: Requires Bash 5+ for EPOCHREALTIME
put__now_millis() {
# $1 is the NAME of the variable to set '__out' could be any variable name
local -n __out=$1
if [[ -n "${EPOCHREALTIME}" ]]; then
local epoch_micro="${EPOCHREALTIME//.}"
__out="$((epoch_micro / 1000))"
else
__out="$(gnu_date +%s%3N)" || throw "put__now_millis: Failed to [gnu_date +%s%3N]"
fi
}
export -f put__now_millis
Example usage:
foo() {
local start_millis end_millis
put__now_millis start_millis
sleep 0.1
put__now_millis end_millis
echo "Time for execution: $((end_millis - start_millis)) ms"
}
Example timing difference of name ref vs $() showing 100X improvement
Timing Code
foo() {
local start_millis
for i in {1..1000} ; do
put__now_millis start_millis
done
}
bar() {
local start_millis
for i in {1..1000} ; do
start_millis="$(now_millis)"
done
}
main() {
echo "---------------------------------------------------------------"
echo "Time 1000 put__now_millis calls"
time foo
echo "---------------------------------------------------------------"
echo "Time 1000 \$(now_millis) calls"
time bar
}
main "${@}" || exit 1
Timing Output
Time 1000 put__now_millis calls
real 0m0.013s
user 0m0.013s
sys 0m0.000s
---------------------------------------------------------------
Time 1000 $(now_millis) calls
real 0m1.813s
user 0m0.135s
sys 0m1.710s
Official Documentation
A variable can be assigned the nameref attribute using the -n option to the declare or local builtin commands (see the descriptions of declare and local below) to create a nameref, or a reference to another variable. This allows variables to be manipulated indirectly. Whenever the nameref variable is referenced, assigned to, unset, or has its attributes modified (other than using or changing the nameref attribute itself), the operation is actually performed on the variable specified by the nameref variable's value. A nameref is commonly used within shell functions to refer to a variable whose name is passed as an argument to the function. For instance, if a variable name is passed to a shell function as its first argument, running
declare -n ref=$1
inside the function creates a local nameref variable ref whose value is the variable name passed as the first argument. References and assignments to ref, and changes to its attributes, are treated as references, assignments, and attribute modifications to the variable whose name was passed as $1. If the control variable in a for loop has the nameref attribute, the list of words can be a list of shell variables, and a name reference is established for each word in the list, in turn, when the loop is executed. Array variables cannot be given the nameref attribute. However, nameref variables can reference array variables and subscripted array variables. Namerefs can be unset using the -n option to the unset builtin. Otherwise, if unset is executed with the name of a nameref variable as an argument, the variable referenced by the nameref variable is unset. - bash manpage
Requires modern bash
- Does require Bash 4.3+
Children
Backlinks