Why You Are Wasting Your Time Making Your Code Performant

Published: (March 8, 2026 at 01:09 PM EDT)
6 min read
Source: Dev.to

Source: Dev.to

The data

// Note to really senior developers: I'm aware that Double is not an
// appropriate data type for prices, but I'm using it for the sake of simplicity.
case class Product(name: String, price: Double)

val products = List(
  Product("Apple",  50),
  Product("Banana", 21),
  Product("Orange", 70)
)

Functional version

def applyDiscount(discount: Double)(product: Product) =
  product.price * (1 - discount)

def priceGreaterThan(limit: Double)(price: Double) =
  price > limit

def calculateTotalPrice(firstPrice: Double, secondPrice: Double) =
  firstPrice + secondPrice

val totalPrice = products
  .map(applyDiscount(0.1))          // apply 10 % discount
  .filter(priceGreaterThan(20))     // keep only prices > 20
  .reduce(calculateTotalPrice)      // sum the remaining prices

Imperative version

var totalPrice = 0.0
for product  20 then
    totalPrice += discountedPrice

The original debate

The original poster (ignoring the Hic sunt dracones annotation) asked which option looked better, stating a mild preference for the functional version because of its readability and intention‑conveying style.

Predictably, the discussion quickly turned to performance. Some senior developers claimed that the functional code had O(3 n) time complexity, while the imperative code had O(n).

Note: “O(3 n) is not a thing. Period.”

Their reasoning: the functional pipeline traverses the collection three times (once for map, once for filter, once for reduce), whereas the imperative version loops only once.

Why “O(3 n)” is a mis‑statement

Big‑O notation describes asymptotic growth as the input size → ∞.
All linear functions—n/150, n, 3 n, 12 500 000 n—are Θ(n), i.e., they belong to the same complexity class. The constant factor (3, 12 500 000, etc.) is ignored when we talk about Big‑O.

A simple performance model

Assume three operations per element, taking times a, b, and c.
Dereferencing an element takes time d.

ScenarioTotal time for n elements
Three passes3 d n + a n + b n + c n = (3 d + ℓ) n
One passd n + a n + b n + c n = (d + ℓ) n

where ℓ = a + b + c.

The ratio of the two runtimes is

[ \frac{3d + ℓ}{d + ℓ} ]

Only if ℓ = 0 (i.e., no work inside the loop) does the three‑pass version become three times slower.
If the work inside the loop is an order of magnitude larger than the dereference cost (ℓ = 10 d), the ratio is 13/11 ≈ 1.18 → an 18 % slowdown, not a 300 % slowdown.

Legibility vs. performance

The curried functions in the functional version improve legibility by clearly conveying intent.
If you prefer a more concise functional pipeline, you can replace the curried helpers with anonymous functions (or method references) and use sum instead of reduce:

val totalPrice = products
  .map(_.price * 0.9)   // apply discount
  .filter(_ > 20)       // keep only prices > 20
  .sum                  // total

Space complexity

When using the naïve functional pipeline, each transformation creates an intermediate collection:

  1. map → collection of size n
  2. filter → collection of size ≤ n
  3. sum consumes the filtered collection

Thus the naïve approach uses O(n) additional space (the intermediate collections).
In practice, many Scala collections (e.g., view, Iterator) can fuse these operations to avoid materialising intermediates, achieving O(1) extra space.

Bottom line

  • Time complexity: Both versions are Θ(n). The constant‑factor difference is usually modest.
  • Space usage: The naïve functional pipeline allocates intermediate collections, but fused/streamed versions eliminate that overhead.
  • Readability: Functional code can be more declarative and expressive, especially with well‑named helper functions.

Both styles have their place; choose the one that best fits the project’s performance constraints and readability goals.

Lazy Evaluation in Scala Collections

In any case, unless we are doing some kind of nested transformations, we will have a solution whose space grows linearly with the size of the input, and arguably worsened by the number of steps in the pipeline.

Once we reach this point I would just include a method invocation in the functional code to finally settle the question:

val totalPrice = products.view
  .map(_.price * 0.9)
  .filter(_ > 20)
  .sum

Do you see that .view method there? That’s the Scala way of switching to lazy evaluation in collections, with the use of Views.

In contrast to strict evaluation—where all the transformations are eagerly applied, producing new structures—lazy evaluation delays computations until they are strictly needed.

  • Map and filter return another View (wrapping the underlying view and remembering the closure needed to compute values) without performing any actual operation.
  • When we finally call sum, which needs to access the elements to produce its output, the real computation happens.

Thanks to lazy evaluation, we loop through the original input only once, without creating any intermediate structures, making this functional code effectively equivalent to the single‑loop imperative code we saw at the beginning.

Trade‑offs

Lazy evaluation isn’t free:

  • It creates intermediate view structures.
  • It creates and stores intermediate closures.

For small input sizes, these overheads may be more costly than simply performing the “loop” several times and creating intermediate collections.

Languages and Evaluation Strategies

  • Haskell – lazy (non‑strict) by default.
  • Scala – strict by default, but offers mechanisms (like .view) to switch to lazy evaluation.

Seniority, Code Length, and Optimization

As I said at the beginning of this article, seniority often seems to be:

  • Directly proportional to the number of language features you know.
  • Inversely proportional to the length of the code you generate.

It also appears linked to your ability to micro‑optimise code, turning an O(n) solution into an O(3n) one (kidding again).

What Really Matters in a Business Environment?

The valuable developer is the one who lowers the total cost of ownership of a software product—i.e., saves the company money.

Where Should We Focus Our Efforts?

According to industry data (quoted from an O’Reilly author):

  • 60 % of software lifecycle costs come from maintenance.
  • Within maintenance, 60 % of costs relate to user‑generated enhancements (changing requirements).
  • 17 % of maintenance costs are due to bug fixes.

Thus, maintenance costs far outweigh operational costs (the infrastructure needed to run the software). Unless you’re Google—where performance gains affect millions of machines—you should prioritize:

  1. Maintainability
  2. Bug‑free code

This means stopping micro‑optimisation for negligible performance gains and instead focusing on writing code that tells a beautiful story—one that fellow developers and your future self will enjoy reading and evolving. Functional programming often has an edge here, but that’s a topic for another post.

“Always code as if the guy who ends up maintaining your code will be a violent psychopath who knows where you live.”
Martin Golding (as quoted in the article)

0 views
Back to Blog

Related posts

Read more »