[No Audio]
In this tutorial, we will be talking about
performance improvements that are possible
with the help of Clippy provided lints.
Lints are warnings, or suggestions provided
by the Rust compiler to help you write more
idiomatic and correct code. Lints can catch
common mistakes or code patterns that may
lead to bugs or sub optimal performance.
There are many built in lints in Rust, and
you can also define your own custom lints.
Let us look at some of the available lints.
[No Audio]
There are many lints available, each lint is
associated with a level, which can be either
allow, warn, or deny. A lint with the level
allow will do nothing by default. These lints
exist mostly, mostly to be manually turned on
via configuration. The warn lint level will
produce a warning if you violate the lint. A
deny lint produce an error if you violate
it. Please note that None is not the lint
level, but somewhat is appearing here in the
interface. The lints are grouped into various
categories, we are interested in the
performance related lints, so let me select them.
[No Audio]
All these lints have the level of
warn, which means that violating these will
cause a warning message. If you scroll down
there, there are a whole bunch of these.
Clicking a specific lint will show its full
details along with examples and explanations.
In this tutorial, we will be going through
some of these which are more common and
frequently encountered. Of course, we cannot
go through all of them, as it will require a
lot of time. The first lint is related to the
use of the Box. Let us look at an example, I
will define a struct containing a single field.
[No Audio]
Let us run the clippy to see if there are any issues.
[No Audio]
It gives out a warning
message, saying that you seem to be trying to
use Box Vec, consider using just Vec. If we
look at the next line, it has some very good
explanation. It says Vec is already on the
heap. Box Vec makes an extra location
since collections already keep their contents
in a separate area on the heap. So if we Box
them, we are just unnecessarily adding
another level of indirection without any
benefit whatsoever. So a more efficient
syntax would be to keep the field as a simple
Vec of i32 values. In general, for better
performance, we should avoid the location on
the heap, since heap management is typically
being done using the operating system. In the
same way, unnecessarily boxing a value is not
a good practice from performance perspective.
For instance, let me define a u32
primitive which is being boxed.
[No Audio]
This should be avoided. The lint which checks for such
issues is Box_local lint. In
general, we should only Box a type when
there is a compelling reason to do so. We
have seen some use cases of the Box such as
undetermined size at compile time due to
recursive types. Let us look at one more use
case where we should be using the Box. This
use case is explained in the lint of
large_enum_variant. First, I
will comment out the previous code. Consider
an enum with two variants
[No Audio]
The first variant takes
only a few bytes, but the second
variant will occupy quite a large space in
memory. Now, when we create an instance of
the enum, it will allocate to it the memory
equal to the largest variant size. If the
large size is occurring very infrequently,
then we will be wasting quite a lot of memory
space making unnecessary allocations. This
can be improved by making the use of the Box
smart pointer, let us Box the large variant.
The Box in this case will take a space equal
to the size of the pointer which is pointing
to some resource on the heap, which is way to
less than the previous size. Please note that
this implementation may not be always
feasible and depends on the usage of the enum
inside the code. For instance, if the
frequency of the variant_2 is
99%, then we will be making quite frequent
heap allocations which are again expensive.
So it all depends on the specific situation
and scenario. A similar lint to this one is
called result_large_error.
[No Audio]
As we are aware that result is also an
enum, so defining an error type for the
result which occupies a large amount of space
can also be boxed. Since the error are
generally quite a few in number, so therefore
boxing the error will be most of the time
beneficial and will provide useful
performance benefits. The remaining of the
details of that lint is very similar, so
therefore, we will skip its details. The key
idea is that box should be used when we have
a compelling reason to use it. Let us look at
another lint related to function calls.
Consider a case where I would like to create
a heap allocated i32 integer with a default value of zero.
[No Audio]
In this line of code we are
making two function calls. The first one is
that of a new, which will make a new Box which
will be making a new heap allocation, and then
we are calling the default which will set the
default value. This syntax can be improved, so
that it makes just a single function call.
Let us revise this.
[No Audio]
This syntax. is now more
efficient from performance perspective,
because it is only making a single call
instead of making two function calls, and
secondly, it is more simplified and contains
lesser code. The general idea is to reduce
unnecessary function calls. A side note in
this regards is that, when we make a function
called the program has to start execution
from a different place in the code, which
will require to store the local variables
into registers and keep track of the place
where to go back after completion. All this
is expensive compared to sequential execution
of a program. This does not however, means
that function that functions are always made.
We need to make a trade off between the
clarity or understanding of our code and its
performance. The more clearer and more
understandable the code is, the more it is
easy to update and maintain. However, in some
situations where performance is very
critical, we may compromise to a certain
level on the number of functions in a
program. In this case, however, the point we
want to emphasize is to avoid unnecessary
function calls. Sometimes we may
unnecessarily make an own copy for doing some
operations which has never been used later on
in the program. Let us consider a string
variable and a string slice variable.
[No Audio]
We want to compare if the two values are equal using
the comparison operator, we may note that the
two variables are of different types. So let
us convert the variable own string type and
then make the comparison.
[No Audio]
The comparison operator can operate on a reference,
so creating an own value effectively throws it
away directly afterwards, which is needlessly
consuming code and heap space. This means
that the allocation is never being assigned
to anything, and is only being used within
this line and will be thrown away after this
line. We may more effectively do the
comparison on references.
[No Audio]
This revised syntax
is more efficient and more effective.
The next lint is extend_with_drain.
This lint relates to the
two methods of extend and append on vectors,
which can both be used for adding items of
one vector to another vector. Let us define
a couple of vectors first.
[No Audio]
The extend methods
extends our collection with the contents of
an iterator. In this case, we would like to
extend a vector a with with an iterator over
the elements of b, so let us use this.
[No Audio]
The drain method returns an iterator that keeps a
mutable borrow on the vector to optimize its
implementation. The extend function add
items one after another, and is therefore
optimal, especially in situation where we
would like the operation to be done at once
and not for each item individually.
The lint therefore suggested to use the append
function, which will add all the elements at once.
[No Audio]
You may note that the append is more
simpler and requires only mutable reference,
while the external requires you to first
provide an iterator, and then it will turn the
references to the items of the iterator to
add it to the current collection. Please note
that if your specific case demands to perform
any actions on the items before adding them,
then it's usually better to use extend.
However, if you only want to add the items
directly then it's better to use the append
for speed and efficiency. Let us cover a few
more. The next lint is collapsible string
replace. Consider a scenario in which we want
to replace some characters in any input
string. We can use the replace function for this purpose.
[No Audio]
These consecutive calls to string replace can
be collapsed into a single call. This can can
be done by calling the replace once and
giving it in an array of characters instead
of individual characters.
[No Audio]
This is more efficient for a couple of
reasons. First, it involves a lesser number
of function calls, which as pointed out
earlier are expensive. Secondly, consecutive
calls to string replace will scan the string
multiple times, which will also lead to extra
cost. There are many other useful lints which
you may consider for improving the
performance of your Rust code. The complete
list is available at this particular link
that is rust-lang.github.io/rust-clippy/master.
I would strongly encourage you
to go through it. With this we end this
tutorial. See you again with more performance
related tutorials and until next tutorial,
enjoy Rust programming.