I tested both of your versions and updated the app
function with:
for (let i = 0; i < 200000; i++) {
Logger.error('Error occurred at ' + new Date());
Logger.info('App initiated at ' + new Date());
}
(or using lambdas)
I removed the console.log
while ensuring to still call message()
in the lambda version.
The lambda version executes in about 0.5 seconds, while the non-lambda version takes around 1 second.
It appears that Lambdas do make a difference in performance. However, the overhead of using console.log
is higher than combining strings and new Date()
. When I reintroduced console.log
, both versions performed equally. If your logs are not frequently triggered (e.g., if level
is set to info
in production but there are many Logger.error
calls), utilizing Lambdas can improve speed.
I compared two versions of your code where the main loop only has Logger.info
when Logger.level
is error
(meaning no logging occurs). In this scenario, the lambda version is approximately 15 times faster than the non-lambda version. The primary performance impact comes from calling new Date()
due to invocation and memory allocation.
The optimal approach would likely involve placing new Date()
inside the error
and info
functions. String concatenation is highly efficient in V8 because it utilizes "cons strings": when you concatenate "aaaaaaaaa"
with "bbbbbbbbb"
, V8 creates a ConsString object that references both substrings separately. The collapsing into a sequential string occurs only when necessary, such as during printing.
Although the question was marked with the "compilation" tag, here are insights on how V8 could potentially equalize the speed of both versions (with and without lambdas) through inlining, constant folding, dead code elimination, and optimizations/deoptimizations. Here's an overview of how this mechanism might operate.
Inlining. Turbofan typically inlines small functions, and may also inline larger functions if sufficient inlining budget is available. Inside your app
function,
Logger.error('Error occurred at ' + new Date());
Logger.info('App initiated at ' + new Date());
is usually inlined. Subsequently, app
essentially looks like:
if (this.level === 'error') {
console.error('Error occurred at ' + new Date());
}
if (this.level === 'info') {
console.info('App initiated at ' + new Date());
}
Following this, two possibilities arise:
Constant folding. Turbofan might identify that this.level
is consistent (has been assigned a value and remains unchanged), resulting in replacing this.level
with its value. Consequently, checks become <some value> === 'error'
and <some value> === 'info'
. These checks are evaluated at compile time, leading to the replacement with true
or false
. Unreachable IF bodies are then eliminated (termed dead code elimination). Notwithstanding, initialyl,
'Error occurred at ' + new Date()
was part of an argument within the function, resembling:
let message = 'Error occurred at ' + new Date();
if (this.level === 'error') {
console.log(message);
}
Hence, dead code elimination predominantly eradicates console.log(message)
, and possibly discards the string concatenation (since the result is unused), leaving out new Date()
due to lacking comprehension regarding its side effects.
Note that speculative constant folding entails some risk: any alteration in this.level
later prompts V8 to discard the Turbofan-generated code (being incorrect) and trigger recompilation.
If Turbofan cannot speculate on this.value
(either due to inconsistency or varied console.error
calls across different Logger
objects), optimization is still plausible.
Evidently, Turbofan refrains from generating assembly code for JS sections devoid of feedback. Instead, it produces deoptimizations, special instructions enabling reverting to the interpreter. Following inlining, provided each run of the function sets this.level
to info
, Turbofan might form something akin to:
if (this.level === 'error') {
Deoptimize();
}
if (this.level === 'info') {
console.info('App started at ' + new Date());
}
Subsequently,
'Error occurred at ' + new Date()
gets completely excised from the graph, thereby being "free". Any modification to
this.level</code activates the <code>Deoptimize()
instruction, reverting back to bytecode interpretation for executing
console.error('Error occurred at ' + new Date());
.
While theoretically viable, achieving this isn't straightforward as the initial computation of
'Error occurred at ' + new Date()
precedes the
if
condition. Therefore, the graph more closely resembles:
let message = 'Error occurred at ' + new Date();
if (this.level === 'error') {
Deoptimize(message)
}
(the Deoptimization consistently requires inputs describing extant values within the function).
At this juncture, potential code motion could facilitate shifting
let message = 'Error occurred at ' + new Date();
into the
if
block, preventing premature computation when unnecessary. Nevertheless, Turbofan’s code motions' efficacy is modest, failing to encapsulate
new Date()
adequately.
A closing remark: Turbofan often conducts concatenation of fixed strings at compilation stage. While irrelevant to + Date()
, benchmarking
"hello " + "world"
mirrors assessing
"hello world"
.