1. Understanding performance bottlenecks
Hello! In this video, we're going to be understanding performance bottlenecks in Java.
2. What are performance bottlenecks?
So, what are performance bottlenecks? Performance bottlenecks are constraints that limit our application's speed and efficiency.
Imagine pouring water through a funnel - the narrowest part determines the flow rate. Similarly, in software, the slowest component often determines overall performance.
3. Types of bottlenecks
Java applications typically experience three main types of bottlenecks. CPU-bound bottlenecks occur when our application is limited by processing power, often due to complex calculations or inefficient algorithms. I/O-bound bottlenecks arise when our application spends significant time waiting for external resources like databases, network responses, or file operations. In this lesson, we'll focus on measuring time-related bottlenecks - specifically CPU and I/O bottlenecks - using `System.nanoTime()`. Memory-bound bottlenecks are equally important but will be covered in a later video when we explore memory monitoring techniques. For now, let's concentrate on how to accurately measure code execution time.
4. Measuring performance with System.nanoTime()
Java provides `System.nanoTime()` for high-precision timing measurements. Java also provides `System.currentTimeMillis()` - however, that gives the current time as we'd see on a clock on the wall (hence "wall clock time"). Instead, `nanoTime()` is designed specifically for measuring elapsed time between two points in code execution. Wall clock time can be affected by system clock adjustments or daylight saving changes, while `nanoTime()` provides consistent measurements of how much time has passed. It returns a value in nanoseconds as a `long` number - that's billionths of a second - giving us very precise measurements. The basic pattern is to capture the start time, run the code we want to measure, capture the end time, and calculate the difference. Let's look at this in action with a practical example.
5. Using System.nanoTime()
Here, we're measuring how long it takes to find an element in an `ArrayList` using the `contains()` method. First, we create a list with 100,000 integers. Then we capture the start time with `System.nanoTime()`, perform our operation - searching for a number at the end of the list - and record the end time. The difference gives us the execution time in nanoseconds.
6. Using System.nanoTime()
We can convert to milliseconds by dividing by one million. This simple technique lets us quantify exactly how long operations take, in milliseconds. Note that `ArrayList.contains()` is an O(n) operation that needs to potentially scan all elements, which is why it's worth measuring.
7. Best practices for performance measurement
When measuring performance, it's crucial to apply some best practices through code.
First, we should always run multiple measurements when timing our code, to help eliminate random fluctuations that could affect a single run.
8. Run multiple measurements
Here, we're running multiple measurements by using a loop to test our search algorithms 3 times. This helps eliminate random fluctuations that could affect a single run. Notice how we sum the execution times and calculate an average.
9. Best practices for performance measurement
Second, when comparing two approaches, we should try to compare their relative difference, as absolute numbers can vary wildly between machines.
Here, we compare the relative performance of two different operations, A and B. By dividing the average times, we can express the performance difference as a ratio, which is much more meaningful than just looking at raw nanoseconds.
10. Let's practice!
Now, let's try to use our newly-learned skills with some hands-on problem solving!