Conf42 Rustlang 2023 - Online

Rust for Java Programmers - a Personal Perspective

Video size:

Abstract

Java has been the de facto business programming language for a long time. On the other hand Rust is often presented as a systems language, a replacement for C++. In this presentation we will look if Rust does have a chance of taking over from Java.

Summary

  • The rusts is a modern system programming language. It is gaining popularity due to its ability to build safe, fast, and reliable software. It has its own unique approach to memory management without using the garbage collector.
  • Ross: What if you actually want to modify in some cases, and in some other cases if don't modify. What if we want a function not to modify it? So let's explore that direction. And this behavior is something that we should be able to write out in the language.
  • 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. Many of these optimization you do not need to write yourself.
  • ROS 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. By utilizing constant and static variable rusts compiler can perform various optimization, including constant propagation.
  • There are some notable projects that are already using roast for the operating system. Other applications of rusts something like in databases, game development, embedded system blockchain and cryptocurrency. Most of the optimization I want to do in rusts has already been done by the developers.

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.
...

Ovidiu Ionescu

Full Stack Developer @ Ahold

Ovidiu Ionescu's LinkedIn account



Awesome tech events for

Priority access to all content

Video hallway track

Community chat

Exclusive promotions and giveaways