When writing code, the order of condition checks can significantly impact the correctness and efficiency of your program. Let's explore this through a recent update I made to Skaffold's source code, which led to a 50% performance increase, but before diving into the details, which option would you prefer?
Option 1:
if util.IsEmptyDir(path) || !info.IsDir()
Option 2:
if !info.IsDir() || util.IsEmptyDir(path)
Check the functions:
util.IsEmptyDir(path)
: This function checks if a directory at the specified path is empty.func IsEmptyDir(path string) bool {
d, err := os.Open(path)
if err != nil {
return false
}
defer d.Close()
if _, err := d.Readdirnames(1); err == io.EOF {
return true
}
return false
}
info.IsDir()
: This method checks if the current instance of the struct is a directory.func (x X) IsDir() bool {
return x.modeType & os.ModeDir != 0
}
Let's remember that in Go (as in many other programming languages), the order of condition checks in an if
statement is evaluated from left to right. This means that the first condition is evaluated before the second. If the first condition returns true, the second condition will not be executed and it’s important.
For example:
if !info.IsDir() || util.IsEmptyDir(path) {}
Here, !info.IsDir()
is evaluated first. If it returns true
, the util.IsEmptyDir(path)
is not evaluated. But why is that so important?
info.IsDir()
method makes only static checks, and this operation is pretty fast while the util.IsEmptyDir()
makes I/O operations; it opens the given path and reads directory names.
When Go performs I/O operations like os.Open(path)
or Readdirnames
, several things happen:
File Descriptor Management:
When you open a file with os.Open(path)
, Go uses system calls like open
(on Unix-like systems) to obtain a file descriptor, which represents an open file in the operating system.
System Calls:
Performing I/O operations involves making system calls to the underlying operating system. For example, os.Open(path)
will ultimately call the open system call to open the file at the specified path.
Blocking Nature: Many I/O operations in Go are blocked by default, meaning the program waits until the operation completes before proceeding.
Taking into consideration all the information above, the right answer here is the second option, for checking !info.IsDir()
first, unnecessary calls to IsEmptyDir
on non-directory paths are avoided. This reduces the number of costly I/O operations, in my case:
First build: from 102,958 to 20,764 calls
On file change: from 411,832 to 103,820 calls
Based on our experience, here are some practical guidelines for optimizing condition order in your code: