🧹Clean Code Is Slow, but You Need It Anyway…
date
Oct 16, 2023
slug
clean-code-is-slow-but-you-still-need-it-anyway
status
Published
tags
Chia sáş»
Sưu tầm
Study
summary
If you are trying to squeeze every nanosecond from a battery of GPUs, then clean code may not be for you; at least in the most taxing of your deepest inner loops. On the other hand, if you are trying to squeeze every man-hour of productivity from a software development team, then clean code can be an effective strategy towards that end.
type
Post
Beware! Clean Code can clean anything. All images from Unsplash
In case you haven’t heard, something really interesting is happening in the programming/software development scene on Twitter. It started from one of Casey Muratori’s videos on Performance Aware Programming. It is aptly titled: “Clean Code, Horrible Performance.” It stemmed from Casey’s own criticism of modern software that runs at less than 1/20 of efficiency. This is a valid concern considering inefficiencies are the reason we can’t get nice things (and also the reason John Carmack left Meta). Then Uncle Bob (i.e., Robert C. Martin) chimed in and had a very thorough and educational discussion with Casey using GitHub. The discussion is still going on as of this day. It also spawned a thread in Hacker News.
Clean Code Is Indeed Slow
I have seen the video, and I think, although he is correct, he wasn’t really painting the whole picture correctly. I take from his video that polymorphism and (excessive) single responsibility principle are slow (and thus, bad!), which is true, in a way. Since with polymorphism, there are costs for vtable lookup and indirections. Not to mention it will make the function and/or data scattered around and not near each other, so it will cause an L1 / L2 CPU cache miss. The same as SRPs. Extracting a function into multiple private helper functions might also cause cache misses. So do inline functions. Unfortunately, polymorphism is the heart of the SOLID principle—one of the key principles of clean code.
I’ve finished my ramen. Why is my code still processing?
Performance Is Indeed Important
Efficiency and performance are pretty crucial in performance-critical software like games or embedded systems since no one would want to buy a 10 FPS game or a slow embedded device. They might also be important for backends to server content as fast as possible.
An inefficient app, for instance, will make your device slow and hot and consume more energy than it should. For the backend, it might add to the costs of cores and/or memories or additional services. I’d reckon this will be something to think about in the future since startups can’t really spend more than they should. The operational cost will be smaller if you can make efficient code.
There are many ways to make your code performant:
- The first is to choose the right algorithm. This may be the easiest one. Choosing the right algorithm might be the difference in getting the code to run like The Flash or Eeyore. So, brush your algorithm up!
- Next, we can get help from the hardware itself. Like using the L1/L2 caches or the CPU to make your code run faster. L1 cache can run 100 times faster than your RAM, and L2 can run 25 times faster. But to do that, we must know how the function and data are laid out in the memory since you need them to be close to one another for the cache to take effect. We can do this by using Data-Oriented Design or Entity-Component-System patterns. You can read this link if you want to know more about caches.
- We can also get help from the CPU’s instruction set extensions like MMX, SSE, or AVX.
The first rule of performance is it should be fast. The second rule of performance is it should be faster than that.
But Clean Code Is Also Important
There are other paths to pursue than pursuing only performance, though. I have seen lots of badly written and unreadable codes. And most of them are from my time as a game developer. As you may know, game developers are notorious for pursuing performant code. Because it means you can push for more content, quality, tris count, textures, etc., to make the game looks better without jeopardizing the FPS.
But I believe that if your only concern is performance, you might end up with a buggy end product like The Source Engine. The infamous “coconut.jpg” defect is probably the result of a violation of the Separation of Concern/Single Responsibility Principle. So, if you change one part of the code, it might affect another part that has nothing to do with the part you are working on. The same happened with me and Blackberry Messenger. There is a class in BBM iOS called BBMessage. We are forbidden to touch the class if not necessary. And if we do need to work on it, thread carefully because it might break other things.
Performant code is not the easiest to read and maintain. If any, it’s often the opposite. Try looking at the “Quake’s Fast Inverse Square Root” code to get the idea. Although not all code is that obscure, some will probably do.
Clean code and other modern programming principles are mostly concerned with the development process rather than performance. They are used to cut programming time rather than performance. Their goal is to make the code more readable, testable, robust, extensible, and maintainable. Sometimes (most of the time?), at the cost of performance. The reason is processor and memory are cheaper than development and maintenance time. So, adding a processor and memory is recommended to compensate for the lack of efficiency clean code creates.
I have to say, when I switched my career from a game developer to a mobile (iOS mainly) developer in KMK Labs / Vidio.com, I got a pretty big shock because the paradigm in the office was different. So, I’ve had a pretty hard time keeping up. Clean and maintainable is preferred over performance. The development process’s speed is more important than the end product’s (but still in an acceptable value). So I’ve got to learn about clean code, SOLID, clean architecture, TDD, MVVM, extreme programming, etc.
The result is enormous, though. The development process is better. It is (arguably) faster, and we’ve got fewer bugs. Team members can hop in and do their work in a class without worrying that they might affect other people’s work. The code is easy to read, and the cognitive load on the brain is lighter. We can extend the feature easily. We can test them easily.
“Yo, is this one good? I did it as clean as possible so you can read it at a glance.”
So What Should We Do?
Being pragmatic and flexible is the key. We must mind both the performance and cleanliness of our code. The code's cleanliness, readability, extensibility, and robustness are more important than performance. Since most of our routines are not that resource intensive, we can get away with some performance reduction and put in better-structured code. This works except if we consider the burden our code will cause on our user’s device. How it will drain its battery, make it hot in a second, etc.
But we also must understand which part of our app needs performance. It is usually in the deepest layer of our app. Where it is called hundreds/thousands and hundred thousands/millions of times. Then, we discard clean code for some performance-enhancing methodologies. So in a way, what we should do are:
- Know the basics: we don’t need to understand what is happening inside the computer deeply. We must know what is happening under the hood, good enough to judge how to work around our code. We also need to know the basics of our language and tools.
- Know the domain: domain in software engineering commonly refers to the subject area on which the application is intended to apply. By having a deep understanding of the domain, we can understand what a certain code is trying to tackle. And then we can code it to better suit the domain’s needs. For instance, in the domain of a hospital queueing app, we don’t need nanoseconds of performance. So it’s better to reduce our development time instead. It’s different from a video game domain, where we must render in real time. But even with that, some parts of the code need to do the opposite. Maybe real-time rendering of the queue number for the hospital queueing app needs performance more. Or the higher layer of the game needs to be coded as clean as possible to handle moddings easier.
- Do abstractions correctly:Â with proper domain knowledge, you can create a strategy for approaching the problem. After that, you have to do abstractions on the problem. Abstractions can be defined as hiding complexity and unneeded information for a given context and just making the ones more relevant. Abstraction, in this case, is not just the abstraction as in Object-Oriented Programming. That is just one kind of abstraction. Variable types are abstractions. They are abstracting the bits that constitute the actual value in the memory. Naming a variable is an abstraction. It is abstracting an unknown variable into something more relevant to the current context of the variable. Operations and functions are abstractions of register instructions. Naming an operation or function is an abstraction. Data structures are abstractions. Layers are abstractions. You get the idea.
- Be pragmatic: at the end of the day, programming aims to deliver a solution to the user in the form of apps. Most certainly we need to deliver what the user wants out of our apps or games. Maybe they want a performant game with more than 100+ FPS? Or maybe they want an app with lots of features, reliable, less buggy, and delivered ASAP? We should be pragmatic with the tools that we have because users don’t care how we do our job. You know the saying with a hammer and nails? Yeah, that. Don’t do that.
- Be flexible: what we need to realize is that programming paradigms, patterns, and principles are just tools and guidance to make our code better. They are not rigid rules that must be followed at all times and costs. Not every switch-cases need to be changed to become Open-Closed. Not every object must be changed from “Array of objects” to “Object of arrays” to do DOD. They should be handled case by case. And to recognize which one you should use, you need a deep understanding of the basics and the domain.
- Care more about your code and team: this is maybe self-explanatory. Bad code (i.e., slow and/or dirty code) results from a programmer that doesn’t care. Care for your code, teammates, the product, and the users. Only then will you code as well as possible. Because you are not coding for yourself.
Conclusion
Let me leave you with a quote from the wise Robert C. Martin, a.k.a. Uncle Bob, from his discussion with Casey Muratori:
If you are trying to squeeze every nanosecond from a battery of GPUs, then clean code may not be for you; at least in the most taxing of your deepest inner loops. On the other hand, if you are trying to squeeze every man-hour of productivity from a software development team, then clean code can be an effective strategy towards that end.