Transcript
This transcript was autogenerated. To make changes, submit a PR.
Hi, I'll be handling the topic unleashing the power of
rusts building safe, fast, and reliable
software for the future. The rusts is
a modern system programming language that is quickly gaining
popularity due to its ability to build
safe, fast, and reliable software,
and it achieves this by
different methods. And one of them, which is what we'll basically be
talking about in this presentation, is the ownership and
borrowing system. I'll also talk about few
optimization techniques that rusts has put in place that makes
its system and its language so fast.
I will also discuss some of the applications of rusts
so, memory managed mastery so, in memory
management, every single program that we write needs
a form of memory management because
we always tend to allocate memory and dealcate memory
as the program progress. And if we do not handle
this memory well, we tend to cause things like
memory leaks. Now, a memory leak in a program occurs
when the program unintentionally allocates memory, usually on the
heap during its execution, but fails to
release the de allocated, or de allocate that
memory properly before the program terminates. And this can
cause the allocated memory to stay reserved
and unavailable to other parts of the program or other processes.
And this would gradually increase
and cause memory usage to increase over time.
So let's take this example, a c plus plus
example. So what happens here is we allocate
memory into the helps here. And what happens
when the program ends? Nothing happens to
data and this memory allocated is reserved and other
part of the program, other resources cannot use
this memory. I imagine that we actually wrap
line five with something like a wild true,
so it will continue to allocate memory until the program crashes.
So a simple solution would be to just
be a system to basically de allocate data before
the program terminates, which is what we
introduce here on line six. Delete data about what happens when you
unintentionally do not de allocate memory. Or the program has
been so complex, has been designed to be so complex that
it's hard to keep track of allocated memory and de
allocating them becomes an issue. So what
happens when memory leaks happens? So these are the consequences.
You have reduced available memory. You have slow execution of programs
on your laptop or on your system, rather then you
have possible program crashes. Resource saturation and
maintenance challenge in order to address
this issue of memory leak, the garbage collector
comes to the rescue. The garbage collector basically
does the dirty work of memory management.
The garbage collector is a component of many programming
languages and runtime environment that automates the process
of memory management. It basically
goes through the program automatically to
deallocate objects that are no longer using used.
But the garbage collector has its own
drawbacks. It has a performance overhead, for instance,
because you actually need a runtime to be able to continually
run the garbage collector, and this causes a performance
overhead. And also, we don't know when the
garbage collector is going to run. It means it is not
so, so predictable. At any point in time,
the garbage collector can run and is going to put a strain on the resources
of the program itself and is going to take
more resources than required at that point, and basically halting
the program for a few microseconds. So,
which means using a language that has a
garbage collector is not ideal for
use cases where we need a very low latency
in the program execution itself, because we don't know exactly
when it will happen. And if it happens when we need the
response, it can cause the metric
of the program itself to fail. So in
those set of situations like game development,
for instance, we basically want to use languages like C Plus plus
that we can handle the memory ourselves and
de allocate them. But because of the drawbacks of
both manually handling memory and using the garbage
collectible, the ROS came in with its own unique
approach to memory management.
And it does this without using the garbage collector. And you don't
have to manually deallocate memory,
although it gives you the chance to be able to beat
yourself if you want to. Okay, so it now begs the question,
how does rusts handle memory management? Since these are the
two basic ways that we know to efficiently
manage memory. So it does this through a system
of ownership and borrowing and
lifetimes, and ownership transfers and moves. So we'll basically
be talking about this system
in this presentation. So take this example.
We'll use a few examples to demonstrate
exactly how roast handles its memory.
So if you take this example, it's a program that's
supposed to run, but then this is what happens if we take
a function, take ownership for instance, that has an
argument s what take ownership wants to do is
it wants to containerize its memory.
And basically when it's going out of scope, it will
say, okay, I have a few objects
that I own, and since I know they will not be used in
any other place, they allocate them. So the compiler
will include something like drop, like a function at
the end of the program, or calls drop on the
own variables, basically. So by the time
you finish running the ownership, we are very sure that
on line ten s is dropped, it is the allocated.
But what happens if you try to access that memory
after them, after line three. So on line three you call
deconship. And anything you
pass into this function, deconnership is dropped
here because it is basically moved,
this string is basically moved into this place. And it means that
if we allow this program to run, we have a no pointer
issue in this
particular place. So this is one of the ways that roast handles
it. So to recap, again, it's a
simple philosophy.
Any data I own, the moment I'm going out
of scope, I'm going to destroy them basically
and deallocate the memory.
So that's basically it. So the compiler really
helps to enforce these rules
and make sure we don't have things like no pointer exception
which we would have had here. And this is the errors that
would have gotten from this programming if we had tried to run it. We have,
this move occurs because string has a type
string which does not implements copy traits. And the value is
basically moved into this empty konashi and it doesn't
exist in the scope of does not leave enough to reach line
four, basically. Okay, so what
if we want to use the value returned from
the ownership? So we can basically assign that value
to another variable which lives long enough to
reach it. But it begs the question, okay, what if we still want to
use string after we run take ownership?
So what we do, instead of
moving string into take ownership, what we'll do is that we'll say,
okay, since you would go out of scope
and you are going to destroy that, the main function will
basically tell people ownership, that since take
ownership you are going to go out of scope, and when you do go out
of code, you are going to drop anything I give on to
you. Anything I give you. Why not I lend you a reference
instead and take ownership, can borrow
a reference and it knows that, okay, it doesn't own the reference and
it doesn't destroy it by the end of its own cycle.
So let's take an example which is using this
calculate means. So this is the example,
which is basically like the previous one. And we still have this issue
of string not living long enough to reach
line five. Okay, so the philosophy is line
two, we create strings. Line three, we move
strings into calculate length. When calculated length
reaches line eleven, it goes out of scope and it
calls drop on s. So since you've moved
string into this place, it owns s and
it drops it and basically de allocates the memory.
So we don't have string. Again, it does not leave
to see line four. So by then get to line five and you're trying
to access it, the compiler is going to complain and
tell you the value has already been moved. So basically
to fix this, we can say okay, calculate length instead
of me, calculate length owning any value given
to me, why not I borrow the
value or the reference to that value
instead? So what happens here is calculate length to say,
okay, I just want something borrowed and I'm
going to use it. And when
I'm done I'm not going to dealocate it. So basically by line
twelve, when calculate means runs by line twelve, it checks
for everything it owns. It does not own s, it is a borrowed value,
so it does not diallocate it. And we know in the main function
if that is the philosophy, and if that is how calculate length
is declared, it means that after line four,
after line three, we can still be
sure that this string that is declared
on line two is not the allocated. So basically
it creates a reference and passes it to calculate length. And calculate
length doesn't own it, so it does not de allocate
it, which means that this string itself
can still be used in line five. It still lives long enough to
be able to get to line five. So it's a very simple principle
which allows the rusts system
to be able to manage the memory in a tight knit manner and not
allow any memory to leak. What if want to handle
a scenario that is multitraded, for instance?
Okay, but before we get to that,
let's first talk about different ways
we can handle this. For instance, if you actually want to modify in some cases,
and in some other cases if don't modify. Okay, so this
is a typescript file. It basically
has a class of user that has a first name,
last name and an age, and also a constructor that
takes in these arguments, and a function which
is set h and the set, it just changes the value of each,
that's all. So in our main function we
create a variable online 18 named user,
and we have a function that is
called cannot modify user. So we want this cannot modify user
to work in such a way that it just uses the value from user,
but it does not modify the user object itself.
Okay, I want a way to be able to enforce this
and not be able to change the value of user by mistake.
But what if I do? User set
h in cannot modify user. It changes the value
of the user object itself. It changes value,
and that is not what we want. So to fix this issue in
typescript, we can basically do something like object freeze
and it freezes the values of a user and you
cannot change it after this point. And this fixes the issue
because when we try to run
this program, when it gets to line 27,
it throws an error, which is what we want, that we cannot
assign a value to the
read property. But then we are changing the
behavior of user because we want a function not to modify
it. What if we want another function to modify it? So let's explore
that direction. So if we have another function which
is can modify user now, for instance, and we now
call user set h 44, this function does
not work because we are using object
of freeze. They mix it red only.
And this behavior is something that we should be able to write out
in the language and not do it unintentionally.
Because if we remove this object of freeze, we can mistakenly
modify user inside the cannot modify
user function, which is not what we want. So this is
Ross's approach to this. So we have basically
the same code. We have struct user, which has the first name,
last name and age. And it has a
method which is set age.
So on line 15, we create a function
user and it is made mutable because
on line 22, we can modify it. So when we create
a function cannot modify from line 27 to 29
that takes in a reference to user. So basically it
just borrows user. And anything it does, it does not
modify user. Okay, this function,
and if we try to modify user inside this function,
the program does not compile. The compiler would complain
and basically scream at us. So if we try to do that
inside this cannot modify function and we do user set
h, it does not allow us to modify it because
we are passing a non mutable function, a reference
to cannot modify, which is the bbl we want. And yes,
it's acting exactly do you want and
the same way in this can modify user, we can
use this and mute user. And we
can basically specify that this can modify function
is telling the main function that, okay, if you are passing me
a value, I am going to be able to modify
it. So that's basically what is happening. And we're able to modify
so we can have this implementation and also
be able to efficiently predict how
the behavior is going to, the behavior of the
program is going to be without seeing the implementation
of cannot modify and can modify. So if we have this type
definition, imagine we don't know what's happening
inside it. We can be very sure that when we
call cannot modify, the user object is not modified,
is not modified. And when you call can modify because he's saying,
okay, I would be able to mute it by borrowing immutable
reference. He's saying that, okay, it will be able to modify,
which is very different from the typescript version,
which would not allow us to do that, and the program is
more unpredictable. Okay, so another scenario
where we typically want to use,
another scenario where you would want to
imagine how memory is being passed is in multitraded
programs, and many programs are multitraded.
So if we take this example,
for instance, we basically have the same thing for the strokes and
the animator. So on line 17
we are creating a new user, and on line 23 we are basically creating a
new trade, spawning a new trade and making sure it
runs on line 27. Another thing to notice in rusts,
trades are lazy. So until you pull them,
they are not going to run. So the trade actually starts running on line
27. So what happens is
this user variable is moved into this
scope and we can tell the compiler to
move this variable into this code by specifying this
move keyword. Basically this program
would run as we want it to run. So fine. So when it
gets to line ten, seven to run this and it basically move user. And when
it's done with this, when it gets to line 25,
it dealocates user. Perfect. But what happens
if we have more than one trade?
So we have trade one, trade two.
So let's just move trade. Okay, online. 31,
we are saying, okay, run the first trade.
So we'll come to this trade and we say
we move user into this trade. When we get to line 25,
the trade sees user as evaluated its own,
so it deallocates it. So by the time we come back to line 32
to run this second trade, the user
variable does not exist anymore because it has
already been reallocated by handle by
the first trade, which is not what we want. But the compiler also complains
and tells us we need to.
The value has already been moved into this
first trade scope, so we need a
different way to handle this. So one of
the easiest way to handle this is using an
arc, which is an atomically referenced counting
structure. Basically it's wrapped around the user
and you can create different clones of that which you can now move
into different contexts. So the arc allows you to be able to
create multiple ownership of a particular
object. So if I wrap an arc over this user,
I can clone it, then move the clone value into the
first trade, then clone the second one, then move the second value
into the second trace, basically, and the
program runs as we expect. Okay,
so let's check some optimization techniques
by rusts by the ROS system. So rusts is a program language that
emphasizes safety and safety, performance and
concurrency. It provides a variety of optimization techniques
to help developers write efficient code without
sacrificing safety. And one thing you should notice many
of these optimization you do not need to write
yourself. So let's take the first one, which is the zero cost
abstraction. So the zero cost abstraction in ROS
that when ROS gives us a high level abstraction,
they do it in such a way that it does not incur any
runtime overhead. For instance,
the ownership and borrowing system in
ROS, the ownership and borrowing system in rusts does not
require us to be able to drop
free a particular memory by ourselves,
but it basically abstracts
the manual memory management
by itself. And it does it in such a way that
does not incur any runtime overhead by using
a garbage collector, for instance. Okay,
so another example of zero
cost abstraction is when you are using iterators.
Iterators in roast have
zero cost abstraction. If you compare it in
roast to something like iterators in C sharp or iterators
in Java, the iterators in C sharp and
Java, they are typically slower than you writing out the
iterator yourself. So for instance, if you have an array and
you use an iterator to map the array into something else,
the execution speed in both C sharp and Java is
much lesser than if you actually write out your loop manually
and do your mapping yourself. But in roast,
using the iterator would be faster
than writing it out yourself. Basically, roast features
that every abstraction, every high level abstraction that
is being given to the programmer is optimized
in such a way that if the programmer handwrites
the logic, it will not be as fast or as optimized as
the abstraction that they are giving. So another
way to suggest to the compiler for an optimization
is inline function. So an inline attribute in rust
is a compile directive that tells the compile to inline the function
at the call site. This means that the compiler will copy
the body of the function into the caller's encode
instead of calling the function as a separate entity. This can
improve performance by eliminating the overhead
of the function calls such as stack frame
setup and theorem. The compiler will not
always inline a function that is marked with
the inline attribute. The compiler will make a decision based on the number
of factors such as the size of the function,
the optimization level, and the target architecture.
So this is how to use the inline attributes
in declarative in rusts. So you just
put the inline like is done online one here,
and the compiler knows what to do. It basically copies
the implementation and
paste them here itself. So doing
that would remove the overhead of calling
the function itself. Basically,
using the inline directive is the
best places to use them is when you have the id case would be when
you have smaller functions, that smaller
functions are called multiple times or they are
called frequently in your program. So you can use
the inline declaration attributes
on your function. Okay, so another optimization
technique is using the constant propagation.
So the constant propagation in ROS is basically the
compiler replacing expression. It's basically
evaluates an expression that would
always evaluate into a constant and replacing it with
the value, the evaluated value
at the point of evaluation. So what it basically does
is it would improve performance by eliminating the need to
evaluate the expression at runtime.
So the ROS
compile can propagate constant expression in a number
of ways. For example, if an expression contains a variable
that has been assigned a constant value, the compiler
can replace the variable with its value.
The compiler can also propagate through arithmetic
operations such as addition and multiplication.
For instance, using in this program,
if you uses a const file, you are telling the
compiler that, okay, this is a constant value and it will basically
propagate this value into this place. So the compiler
is also smart enough to see that, okay, radius has not
been reassigned, the value
of radius has not changed. So the compiler would do two
times, five times the value of PI and replace this expression with that
value at compile time. And what that does is it should eliminate
the need for evaluating this at runtime.
Okay, so using the static variable using static variable is very similar
to using the const. But the major difference, or one of
the biggest difference, is that when you are using the static variable,
it basically creates a value on the heap and gives
it a lifetime of static, which means it lives
as long as the program lives. It reallocates this
memory when the program terminates,
basically. And the compiler can also use this value
for constant propagation. As we've discussed in the
previous slide, by utilizing
constant and static variable rusts compiler can perform various
optimization, including constant propagation,
which can lead to more efficient generated code. This optimization can
eliminate unnecessary runtime calculations and improve the
overall performance of Rusts programs. Okay, some usage
of roast we see the usage of roast in operating
system. And yes,
it's not a surprising usage because one of the biggest
issues with operating system is memory leaks.
And there are some notable projects that are already
using roast for the operating system, something like Redux OS
and OS and even Microsoft is also writing the core Windows
libraries in rusts and we have
web servers developers so
because of the advantages that roast brings to the table we
can also use it on savers and we already have
projects like Arctics and Rocket that provides framework
that leverages roast and concurrency model and memory
safety to develop robust web application.
Then we have other applications of rusts something
like in databases, game development, embedded system
blockchain and cryptocurrency so
we also have in networking you can
use roast. So basically for a recap is
that most of the optimization I want to do in rusts has
already been done by the developers of rusts themselves
although there are some instances that we can optimize our code
and to also run
very fast or to improve the performance.
But then rusts has also done a lot of optimization
and we've never even talked about things like unwrapping of loops
and some other optimization technique that rusts used.
So thank you for listening and tuning into this talk.
And also remember with great power comes great responsibility.
Thank you.