You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Text simplification and typographic improvements via @ICR-RSE-Group (#73)
Including an additional figure that was reproduced to improve the accessibility of the font used.
This is a manual merge of approriate changes from PR (#51) due to large conflicts with earlier merged optimisation PRs.
With thanks to the original authors of these changes @msarkis-icr and @stacyrse
Copy file name to clipboardExpand all lines: episodes/optimisation-data-structures-algorithms.md
+15-10Lines changed: 15 additions & 10 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -58,6 +58,11 @@ When an item is appended, the list checks whether it has enough spare space to a
58
58
If it doesn't, it will re-allocate a larger array, copy across the elements, and deallocate the old array.
59
59
The item to be appended is then copied to the end and the counter which tracks the list's length is incremented.
60
60
61
+
<!-- Based on ICR-RSE's visual note: https://icr-rse-group.github.io/carpentry-pando-python/optimisation-data-structures-algorithms.html#lists -->
62
+
{alt="A list uses a contiguous block of memory, similar to an array, for storing the pointers to its elements. It is depicted as a series of five adjacent boxes, labelled 'P1' to 'P5', representing pointers to the list's elements.
63
+
It can have additional storage beyond its length to make appends faster. An illustration shows the previous list with two extra empty boxes marked with question marks, indicating spare elements. Below, Python code `len(my_list) == 5` and `my_list.append(6)` is shown. After appending, the first of the previously empty boxes contains 'P6', and the last one remains empty. The length is now `len(my_list) == 6`. Appending to a full list causes it to grow. This makes some appends slower. An illustration depicts a full list with 'P1' through 'P7' in adjacent boxes and a label "No spare elements!". Below, Python code `len(my_list) == 7` and `my_list.append(8)` is shown. The result is a new, larger continuous block of memory with 'P1' through 'P8' followed by a question mark in an additional box, indicating one spare element. The label "2 new elements" with curved arrows suggests that when the list grows, it typically allocates more memory than just the space for the new element.
64
+
A concluding note states that a list will typically grow by 12.5%, hence shorter lists will grow more frequently when appending." }
65
+
61
66
The amount the internal array grows by is dependent on the particular list implementation's growth factor.
62
67
CPython for example uses [`newsize + (newsize >> 3) + 6`](https://github.com/python/cpython/blob/a571a2fd3fdaeafdfd71f3d80ed5a3b22b63d0f7/Objects/listobject.c#L74), which works out to an over allocation of roughly ~12.5%.
63
68
@@ -152,20 +157,20 @@ Since Python 3.6, the items within a dictionary will iterate in the order that t
152
157
### Hashing Data Structures
153
158
154
159
Python's dictionaries are implemented as hashing data structures.
155
-
Explaining how these work will get a bit technical, so let’s start with an analogy:
160
+
Explaining how these work will get a bit technical, so let's start with an analogy:
156
161
157
162
A Python list is like having a single long bookshelf. When you buy a new book (append a new element to the list), you place it at the far end of the shelf, right after all the previous books.
158
163
159
164
{alt="An image of a single long bookshelf, with a large number of books."}
160
165
161
-
A hashing data structure is more like a bookcase with several shelves, labeled by genre (sci-fi, romance, children’s books, non-fiction,…) and author surname. When you buy a new book by Jules Verne, you might place it on the shelf labeled “Sci-Fi, V–Z”.
162
-
And if you keep adding more books, at some point you’ll move to a larger bookcase with more shelves (and thus more fine-grained sorting), to make sure you don’t have too many books on a single shelf.
166
+
A hashing data structure is more like a bookcase with several shelves, labelled by genre (sci-fi, romance, children's books, non-fiction, …) and author surname. When you buy a new book by Jules Verne, you might place it on the shelf labelled "Sci-Fi, V–Z".
167
+
And if you keep adding more books, at some point you'll move to a larger bookcase with more shelves (and thus more fine-grained sorting), to make sure you don't have too many books on a single shelf.
163
168
164
-
{alt="An image of two bookcases, labelled “Sci-Fi” and “Romance”. Each bookcase contains shelves labelled in alphabetical order, with zero or few books on each shelf."}
169
+
{alt="An image of two bookcases, labelled "Sci-Fi" and "Romance". Each bookcase contains shelves labelled in alphabetical order, with zero or few books on each shelf."}
165
170
166
-
Now, let's say a friend wanted to borrow the book "'—All You Zombies—'" by Robert Heinlein.
171
+
Now, let's say a friend wanted to borrow the book "'—All You Zombies—'" by Robert Heinlein.
167
172
If I had my books arranged on a single bookshelf (in a list), I would have to look through every book I own in order to find it.
168
-
However, if I had a bookcase with several shelves (a hashing data structure), I know immediately that I need to check the shelf “Sci-Fi, G—J”, so I’d be able to find it much more quickly!
173
+
However, if I had a bookcase with several shelves (a hashing data structure), I know immediately that I need to check the shelf "Sci-Fi, G—J", so I'd be able to find it much more quickly!
169
174
170
175
::::::::::::::::::::::::::::::::::::: instructor
171
176
@@ -199,7 +204,7 @@ To retrieve or check for the existence of a key within a hashing data structure,
199
204
200
205
### Keys
201
206
202
-
Keys will typically be a core Python type such as a number or string. However multiple of these can be combined as a Tuple to form a compound key, or a custom class can be used if the methods `__hash__()` and `__eq__()` have been implemented.
207
+
Keys will typically be a core Python type such as a number or string. However, multiple of these can be combined as a Tuple to form a compound key, or a custom class can be used if the methods `__hash__()` and `__eq__()` have been implemented.
203
208
204
209
You can implement `__hash__()` by utilising the ability for Python to hash tuples, avoiding the need to implement a bespoke hash function.
205
210
@@ -301,7 +306,7 @@ Constructing a set with a loop and `add()` (equivalent to a list's `append()`) c
301
306
302
307
The naive list approach is 2200x times slower than the fastest approach, because of how many times the list is searched. This gap will only grow as the number of items increases.
303
308
304
-
Sorting the input list reduces the cost of searching the output list significantly, however it is still 8x slower than the fastest approach. In part because around half of it's runtime is now spent sorting the list.
309
+
Sorting the input list reduces the cost of searching the output list significantly, however it is still 8x slower than the fastest approach. In part because around half of its runtime is now spent sorting the list.
305
310
306
311
```output
307
312
uniqueSet: 0.30ms
@@ -316,9 +321,9 @@ uniqueListSort: 2.67ms
316
321
317
322
Independent of the performance to construct a unique set (as covered in the previous section), it's worth identifying the performance to search the data-structure to retrieve an item or check whether it exists.
318
323
319
-
The performance of a hashing data structure is subject to the load factor and number of collisions. An item that hashes with no collision can be checked almost directly, whereas one with collisions will probe until it finds the correct item or an empty slot. In the worst possible case, whereby all insert items have collided this would mean checking every single item. In practice, hashing data-structures are designed to minimise the chances of this happening and most items should be found or identified as missing with a single access.
324
+
The performance of a hashing data structure is subject to the load factor and number of collisions. An item that hashes with no collision can be accessed almost directly, whereas one with collisions will probe until it finds the correct item or an empty slot. In the worst possible case, whereby all insert items have collided this would mean checking every single item. In practice, hashing data-structures are designed to minimise the chances of this happening and most items should be found or identified as missing on the first attempt (without probing beyond the original hash).
320
325
321
-
In contrast if searching a list or array, the default approach is to start at the first item and check all subsequent items until the correct item has been found. If the correct item is not present, this will require the entire list to be checked. Therefore the worst-case is similar to that of the hashing data-structure, however it is guaranteed in cases where the item is missing. Similarly, on-average we would expect an item to be found half way through the list, meaning that an average search will require checking half of the items.
326
+
In contrast, if searching a list or array, the default approach is to start at the first item and check all subsequent items until the correct item has been found. If the correct item is not present, this will require the entire list to be checked. Therefore the worst-case is similar to that of the hashing data-structure, however it is guaranteed in cases where the item is missing. Similarly, on-average we would expect an item to be found halfway through the list, meaning that an average search will require checking half of the items.
322
327
323
328
If however the list or array is sorted, a binary search can be used. A binary search divides the list in half and checks which half the target item would be found in, this continues recursively until the search is exhausted whereby the item should be found or dismissed. This is significantly faster than performing a linear search of the list, checking a total of `log N` items every time.
Copy file name to clipboardExpand all lines: episodes/optimisation-introduction.md
+14-18Lines changed: 14 additions & 18 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -27,28 +27,29 @@ We'll talk briefly about some of these external bottlenecks at the end. For now,
27
27
In order to optimise code for performance, it is necessary to have an understanding of what a computer is doing to execute it.
28
28
29
29
<!-- Goal is to give you a high level understanding of how your code executes. You don't need to be an expert, even a vague general understanding will leave you in a stronger position. -->
30
-
Even a high-level understanding of how you code executes, such as how Python and the most common data-structures and algorithms are implemented, can help you to identify suboptimal approaches when programming. If you have learned to write code informally out of necessity, to get something to work, it's not uncommon to have collected some "unpythonic" habits along the way that may harm your code's performance.
30
+
A high-level understanding of how your code executes, such as how Python and the most common data-structures and algorithms are implemented, can help you identify suboptimal approaches when programming. If you have learned to write code informally out of necessity, to get something to work, it's not uncommon to have collected some "unpythonic" habits along the way that may harm your code's performance.
31
+
32
+
<!-- This should be considered good practice that you can implement when first writing your code. -->
33
+
These are the first steps in code optimisation, and knowledge you can put into practice by making more informed choices as you write your code and after profiling it.
31
34
32
35
<!-- This is largely high-level/abstract knowledge applicable to the vast majority of programming languages, applies even more strongly if using compiled Python features like numba -->
33
36
The remaining content is often abstract knowledge, that is transferable to the vast majority of programming languages. This is because the hardware architecture, data-structures and algorithms used are common to many languages and they hold some of the greatest influence over performance bottlenecks.
34
37
35
-
## Premature Optimisation
38
+
## Performance vs Maintainability
36
39
37
40
> Programmers waste enormous amounts of time thinking about, or worrying about, the speed of noncritical parts of their programs, and these attempts at efficiency actually have a strong negative impact when debugging and maintenance are considered. We should forget about small efficiencies, say about 97% of the time: **premature optimization is the root of all evil**. Yet we should not pass up our opportunities in that critical 3%. - Donald Knuth
38
41
39
-
This classic quote among computer scientists states; when considering optimisation it is important to focus on the potential impact, both to the performance and maintainability of the code.
40
-
41
-
Profiling is a valuable tool in this cause. Should effort be expended to optimise a component which occupies 1% of the runtime? Or would that time be better spent focusing on the most expensive components?
42
+
This classic quote among computer scientists emphasises the importance of considering both performance and maintainability when optimising code and prioritising your optimisations.
42
43
43
-
Advanced optimisations, mostly outside the scope of this course, can increase the cost of maintenance by obfuscating what code is doing. Even if you are a solo-developer working on private code, your future self should be able to easily comprehend your implementation.
44
+
While advanced optimisations may boost performance, they often come at the cost of making the code harder to understand and maintain. Even if you're working alone on private code, your future self should be able to easily understand the implementation. Hence, when optimising, always weigh the potential impact on both performance and maintainability. While this course does not cover most advanced optimisations, you may already be familiar with and using some.
44
45
45
-
Therefore, the balance between the impact to both performance and maintainability should be considered when optimising code.
46
+
Profiling is a valuable tool for prioritising optimisations. Should effort be expended to optimise a component which occupies 1% of the runtime? Or would that time be better spent optimising the most expensive components?
46
47
47
-
This is not to say, don't consider performance when first writing code. The selection of appropriate algorithms and data-structures covered in this course form good practice, simply don't fret over a need to micro-optimise every small component of the code that you write.
48
+
This doesn't mean you should ignore performance when initially writing code. Choosing the right algorithms and datastructures, as we will discuss in this course, is good practice. However, there's no need to obsess over micro-optimising every tiny component of your code—focus on the bigger picture.
48
49
49
50
### Performance of Python
50
51
51
-
If you've read about different programming languages, you may have heard that there’s a difference between “interpreted” languages (like Python) and "compiled" languages (like C). You may have heard that Python is slow *because* it is an interpreted language.
52
+
If you've read about different programming languages, you may have heard that there’s a difference between "interpreted" languages (like Python) and "compiled" languages (like C). You may have heard that Python is slow *because* it is an interpreted language.
52
53
To understand where this comes from (and how to get around it), let's talk a little bit about how Python works.
53
54
54
55
{alt="A diagram illustrating the difference between integers in C and Python. In C, the integer is a raw number in memory. In Python, it additionally contains a header with metadata."}
@@ -111,22 +112,17 @@ This usually makes your code more readable, too: When someone else reads your co
111
112
## Ensuring Reproducible Results
112
113
113
114
<!-- This is also good practice when optimising your code, to ensure mistakes aren't made -->
114
-
When optimising your code, you are making speculative changes. It's easy to make mistakes, many of which can be subtle. Therefore, it's important to have a strategy in place to check that the outputs remain correct.
115
+
When optimising existing code, you're often making speculative changes, which can lead to subtle mistakes. To ensure that your optimisations aren't also introducing errors, it's crucial to have a strategy for checking that the results remain correct.
115
116
116
-
Testing is hopefully already a seamless part of your research software development process.
117
-
Test can be used to clarify how your software should perform, ensuring that new features work as intended and protecting against unintended changes to old functionality.
118
-
119
-
There are a plethora of methods for testing code.
117
+
Testing should already be an integral part of your development process. It helps clarify expected behaviour, ensures new features are working as intended, and protects against unintended regressions in previously working functionality. Always verify your changes through testing to ensure that the optimisations don’t compromise the correctness of your code.
120
118
121
119
## pytest Overview
122
120
123
-
Most Python developers use the testing package [pytest](https://docs.pytest.org/en/latest/), it's a great place to get started if you're new to testing code.
121
+
There are a plethora of methods for testing code. Most Python developers use the testing package [pytest](https://docs.pytest.org/en/latest/), it's a great place to get started if you're new to testing code.
124
122
125
123
Here's a quick example of how a test can be used to check your function's output against an expected value.
126
124
127
-
Tests should be created within a project's testing directory, by creating files named with the form `test_*.py` or `*_test.py`.
128
-
129
-
pytest looks for these files, when running the test suite.
125
+
Tests should be created within a project's testing directory, by creating files named with the form `test_*.py` or `*_test.py`. pytest looks for file names with these patterns when running the test suite.
130
126
131
127
Within the created test file, any functions named in the form `test*` are considered tests that will be executed by pytest.
Copy file name to clipboardExpand all lines: episodes/optimisation-memory.md
-2Lines changed: 0 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -242,7 +242,6 @@ Within Python memory is not explicitly allocated and deallocated, instead it is
242
242
The below implementation of the [heat-equation](https://en.wikipedia.org/wiki/Heat_equation), reallocates `out_grid`, a large 2 dimensional (500x500) list each time `update()` is called which progresses the model.
243
243
244
244
```python
245
-
import time
246
245
grid_shape = (512, 512)
247
246
248
247
defupdate(grid, a_dt):
@@ -291,7 +290,6 @@ Line # Hits Time Per Hit % Time Line Contents
291
290
If instead `out_grid` is double buffered, such that two buffers are allocated outside the function, which are swapped after each call to update().
0 commit comments