I'll admit I'm completely convinced of Arc over String, because of how common it is to need to clone a string. I'm less convinced about Arc, since I almost never need to clone a vector
btw when Arc or Rc are created from String or &str, they initially clone the contents of the str. That's why we don't need to bother about lifetimes. They are just not involved in this.
@@onebacon_String does implement Deref. You cannot use str raw, because it cannot be put on a stack, as it is unsized, but you can convert String into Box without cloning and operate on it like that
Also note that if you have a bunch of short string ids, a crate like smol_str is a much better solution than this, because its strings are stack-allocated.
Nobody mentioned the difference between Rc vs just &str. For most situations, a simple &str is simpler, but can make some lifetime annotation problems.
impl From for Arc calls into impl From for Arc Which calls into ::from_slice(v) Which calls into impl ArcFromSlice for Arc which calls into Arc::copy_from_slice which calls into Arc::allocate_for_slice which then calls ptr::copy_nonoverlapping So it is taking the string bytes that the &str points to and copying them into the ArcInner allocation, removing the meaning of the lifetime. The ArcInner allocation is managed/owned by the Drop implementation of Arc.
@7:40 I assume it's figured out at some point but a &str is a reference to a string. A str is a sequence of characters. A String owns a str, which contains the actual data. Arc owns the str, it's not Arc. @20:10 This probably doesn't make a noticeable difference, but technically you could have the ptr in each stack instance of the Arc point directly to the data. Then you do a -2*word to access the strong field and -word to access the weak field, because you access the reference count fields less frequently than the data. @22:50 I believe Arc::from(String) works. @26:30 Yeah okay, it does. It copies the str, I believe, since it needs to prepend the refcount fields. A str is not a &str. A [T] is not a &[T]. @29:10 If you're using Arc in a single-threaded program linked against a malloc implementation that isn't thread safe, then if you get extremely lucky, maybe malloc will be faster than an atomic operation. However. If you're using Arc in a multi-threaded program (otherwise, why not use Rc?)... well, it has to be linked against a thread-safe malloc. And a thread-safe malloc needs to acquire locks or at the very least use atomics for double check locking. Thus, your two cases are - you're using an Rc and it's single-threaded: then it's just going to be strictly faster, because incrementing a word is always going to be faster than any non-inlined function call, not speaking of a call to malloc. - you're using an Arc and it's multi-threaded: then it's just going to be strictly faster again, because atomically incrementing a word is never going to be slower than whatever malloc has to do to ensure thread safety, and then malloc also has to other work. For dropping, it's going to be the same. I believe most modern malloc implementations do most of their house-keeping on malloc calls and do rather minimal work on free calls, but the free calls still have to be thread-safe if you're using threads and linked against a thread-safe malloc. Your example about sets vs arrays is a reasonable thing to be cautious about, but in this case, you have to remember that an atomic increment, while potentially expensive compared to a normal one, is the cheapest possible form of thread-safe operation, which means that anything thread-safe (like the version of malloc you have linked in any program using multiple threads) is going to be no faster than Arc. @31:30 I think the thesis of his video is exactly that it is that its telos is to be mutated. If it's reached the point where it's never going to be mutated, but is still sticking around for a while, then you convert it into an Rc/Arc from that point on.
Atomic increments and decrements are EXTREMELY cheap. Not as cheap as a normal increment, but the difference is negligible until false sharing comes into play, but at that point you're already doing thousands of atomic operations on the same integer per second, which is a really unlikely scenario in this case. Either way, it's going to be orders of magnitude faster than having at least two function calls (malloc and memcpy*) plus the time required to actually allocate and copy the memory over. Edit since people are pointing out a couple of oversights: - memcpy is not a syscall, but still a function call unless inlined - malloc is also technically not a syscall but will likely involve one at some point, unless your allocator already has some memory ready for use.
Malloc and memcpy are not syscalls, and allocating memory from the system is done in amortized fashion. I'm not trying to say that that allocating on heap will be generally faster, but for sure you can design a multithreaded scenario where the memory allocator can use thread local alloc impl and be faster than Arc. Arc will most likely be much faster in most cases, but there are exceptions for sure.
And rust is using jemalloc as the allocator, so there are thread local cached allocations. So you are now comparing global atomic ops with thread-local ops.
@@benharris4436 Rust no longer uses jemalloc by default by the end of 2018, now it usually uses whatever the system libc provides (though one can manually enable jemalloc, this is not always possible on all targets).
@@HarryChengv2 I have to say, it is funny that by default, 99% of rust programs are dynamically linked to glibc. yes, you can build it statically or change memory allocators, or even write your own, but it's funny nonetheless.
I think Arc/Arc does not need the original lifetime because it copies the contents of the slice. It's an owned type. See implementation: rust std src/alloc/sync.rs.html#1467 (seems I cannot post the link to docs?)
32:22, yes they're not mutable, but that's exactly the issue, String is the same type wether binded to a mutable or immutable variable, but it's inner structure is mutable. if you have an immutable String you're paying the same cost as you would if it was mutable. a better comparisson to String, rather than Arc, is Box, which imposes the same ownership semantics (single owner), but doesn't have the cost of being mutable (or the cost of being thread-safe) I think it's really confusing that this guy says Arc is better, it's only better if you need multiple ownership and thread capacities. if you won't send it between threads, Rc is better, and if you don't need multiple ownership, Box is better
1:55 That sexy animation looks to me like it's done with manim, the animation library originally developed by 3blue1brown for his math videos, later forked into a community project. Apart from some boilerplate like imports and wrapping things in the correct subclass, making that animation is as easy as t=Text("Some smexy text") play(Write(t))
That was my first impression of Vec and String. That there is no real point of using them unless they're mutable. A different structure would be better in most immutable cases. And even when dealing with things greater than stack size you can use Box instead of Vec.
@@rosehogenson1398 let t: Box = Box::new([value; len]); only works if size is known at compile time. let t: Box = vec![value; len].into(); works without reallocation (Vec has the exact capacity we want) but it's not specified explicitly.
Just started rust, had the ability to clone or pass by reference and clone really felt dirty. I feels at 2:00. The Rust Book is really well put together and I'm enjoying the language for the most part (compiler is super helpful). I just hope internal drama doesn't kill the language.
str is just utf8 compliant [u8] you make a string somewhere, and then you can move its data to make the first Rc/Arc instance ever (idk if its .into() or whatever actually - edit: watched video and saw it is in fact into()), and that will never dealloc until all instances die. so the lifetime is effectively 'static (i dont remember if thats actually the case... actually i think if you pull a reference from an &'a Rc its going to be Rc's lifetime 'a, but inside of Rc its 'static), since if you can access that str, that means you have an instance of an Rc/Arc, and that will not die on you because theres an instance
My question is: why do you need to clone the Arc (or Rc) vs taking a shared reference? (Especially for immutable data see: 1:44). I'm sure that these are situations where you want the clone, but I feel like the fair point of comparison is taking a shared reference rather than cloning the whole vector/string. I guess the last few seconds with the Box he started to get into it, but I feel like the video wasn't super well structured.
If you have a string in one of your struct fields, and want other fields to point to this string, because you don't want to have unnecessary copies, eg. you have a field `HashMap`, and then want to build indexes into the main hashmap, so `HashMap`, and then also maybe you want values inside the hashmap to be able to refer to other values by using a hashmap key, etc. then using `Arc` as `MyStringId` allows you to do that without copying the string every time. And you can't use references because safe Rust doesn't allow self-referential structs. You're still free to return `&str` from the public API and I think that's preferred instead of exposing `Arc`s, which are an implementation detail.
@@Bravo-oo9vd the right solution, in your example, is to allocate a String, which is not owned by the main map. The main map should be of type HashMap. Both &str's are sub slices of the originally allocated String. This video represents a misunderstanding over the purpose of the types. Reference counting and atomics have nothing to do with the problem at hand.
Note: Cloning an Arc will *always* be faster than cloning a Vec or String, because Rust doesn't have small String/Vec optimizations (there are good reasons for that), so there will always be a heap allocation in the String/Vec version, while the relevant parts of the Arc clone (the fat pointer) are stack-allocated (or directly in registers). And the atomic memory access _is_ slow but operationally insignificant compared to malloc (or similar). Also, for the record, a _str_ is not a string slice but a bunch of (valid) unicode bytes with unspecified length. A _&str_ is a girthy pointer to that block. That is why it's not Arc but Arc, the Arc taking the place of the fat pointer, having an allocation of the unicode bytes as its data.
Yerp, you'll find info about str, and friends (like [T], and dyn Trait) under dynamically sized types (DST). DSTs along with zero-sized types (ZST) (looking at them `struct Empty;` decls, and unit maybe?) are what make allocating in Rust so colorful, but give so much power to the language imo.
29:40 The allocator is a shared resource between all threads. So I would assume any allocation/deallocation includes a atomic increment/decrement anyway.
Here's how you do it: let a: Arc = Arc::from("a"); println!("{}", a); //prints: a It's not possible to create a variable of type str directly, because it doesn't implement Sized. But it's posdible to have a pointer to it. &str is one such pointer. It's value is not a string of characters, but instead it's a pointer to the beginning of the string of characters and the length of them. So actual size of &str variable would be double usize. But apart from this pointer type, it's possible to hold str in any pointer: Box, Rc etc. To create such variable, you commonly use a method from, to which you can pass a reference (you can convert a reference to smart pointer). In this case, any string literal is actually a static lifetime reference to a string slice (str) stored inside binary. So you can do it directly, like this Arc::from("string slice"), where "string slice" is of type &str. You'd know this, by reading the Book. The same goes for any slices, but it becomes a lot more obvious in those cases, because they don't use special annotation, like str and instead actually use [T] syntax. You can also take a rawpointer to &str, but you'd need it's length separately, if you want to do anything remotely useful with it: let s = "slice"; let len = s.len(); let p = s as *const str; // immutable let p = s as *mut str; // mutable
I think one major problem/downside of using these arcs is that it makes your code a lot less straight forward/readable. Everyone knows what a Vec stands for, but an Arc seems a lot more foreign. I think optimizations like these can be very helpful in certain situations, but I think that readability is more important in most use cases. Especially since it might only save 1 ms and a couple of bytes on calls that take 100s of ms on modern machines with gigabytes in RAM.
Let foo = String::new("Hello") ; Let arc_foo: Arc = Arc::from(foo) ; Arc::from takes ownership of the String so now the arc manages the memory of the str.
I have recently taken a look at the Rust compiler and you could run into similar cloning conundums, when doing type inference/coerscion. They solved it by using a 'tcx lifetime as arena allocation. Seeing lifetimes used as a marker for memory arenas has given me a new way of looking at lifetimes.
Wasn’t pre-C++11 std::string effectively the equivalent of `Rc` (+ the logic for resizing the buffer) and then it was changed to be the equivalent of `Box` for performance reason, doing the exact opposite of what is presented here? Also, if all you have are read-only strings, why do you need an `Arc/Rc` and not just a reference `&str` ?
A str itself is a sequence of bytes, basically a char[0] in C, it is not a pointer but rather a variable char array. Rc is the same as &str except that it lives until there are no more references left
@@AlexisPaqueswait, isn't utf-32, 4 bytes fixed sized? Why prefer that over utf-8? On a Hella lot of strings that'll be a huge memory hog, especially when almost all strings are ASCII anyway, unless localized I can see a speed benefit though, as it requires less instructions
@@AlexisPaques Strings in Rust are always encoded in UTF-8 and a String is literally a Vec under the hood. A str is comparable to a [u8] with the difference being additional functions and ensured UTF-8.
str is just a compilation guarantee that the underlying [u8] is in valid utf-8 format, thats why as_bytes is const, because it doesnt change anything, it just gives you raw access to the &[u8] the Arc (or Box, or String) is pointing to
26:09 this is what's happening. let foo = String::from("hello world"); let temp: &str = foo.deref(); let foo: Arc = temp.into(); rust is kind enough to call the .deref() method for you.
21:44 There's an educe crate that implements all these for you. You're welcome. Also, &'static str is the king of strings. If you *really* want to get strings from runtime and care a lot about performance, you can consider a non-static &'a str. However, I acknowledge that it poisons your code with
So basically if you turn a String into a str you are moving ownership (and the string memory) to a str data type (which is just an array of bytes) and making it immutable. This means that String is dropped and memory is moved to a str. If you wrap a str in an Arc you basically have the same thing as a &str, but lifetimes are handled at runtime and the owner of the string bytes is the Arc (really there is no owner, the memory is leaked with Box.leak and deleted when the Arc no longer has references). When you drop the Arc the str bytes are dropped too.
21:29 I wanted the derive that derives all the crap, but after finding out that the common derives are hardcoded into rustc and are thus fast, I immediately gave up on having a proc macro.
Another thing to consider is that atomics on x86 and x64 are free on pointer sized values. In fact, regular writes are atomic as well. This means that any slowdown is mainly due to the compiler not being allowed to reorder atomic loads and stores. (And possibly weak references being more complicated in threaded settings.)
Doesn't the Arc have to copy the data to it's own heap memory? It does seem that way. Like he explained in the video, Arc is just a pointer to the ArcInner type, which is a strong and weak count followed by the actual data.
And the reason you have a 16 byte pointer object for [T] is because of Sized. An Arc is just 8 bytes and an Arc is 16, for example. Rust has taught me so much about memory.
This was super interesting and in my expperience is true whenever applications get middle to large size. Memory & memory-cache can be huge deals and as such having flexible data structures can be a absolutely necessary
Vec and String is for mutating. The underlying data is mutable, so as long as you own them (e.g. don't pass &Vec or &String, which you should never do anyways) then you can simply access the inherent mutability of the type by "moving" it: 'let deez_nuts = String::from("goblin"); let mut deez_nuts = deez_nuts;'
@@Turalcar Yes, 'String::from' will copy to the heap, but that's not relevant to my statement. For String (however you chose to construct it) the underlying data on the heap is mutable.
A null terminated string pointer is only half the size of a Box (but the heap allocation for it is 1 byte larger). The amount of indirection is the same for both (= 1). But the null terminated string also has 2 disadvantages: - it can't contain the null character - you must compute the length every time you need it at the cost of O(N). Whereas Box includes the length directly (that's why it's twice as large).
More memory efficient, sure, but less computationally efficient (in case you need to know length of it, which you do pretty often). It's also more error prone (you can just corrupt the nul termination character and you break everything, which isn't that hard in C, since you can randomly access memory and dereference it in C).
If the involved lifetimes are simple enough you can also just use &str. You can use &str as keys in a map as long as the lifetime of the map is guaranteed to be shorter than of what wherever the &str comes from.
So this is like shared_pointer from like C++ or something. Or if you were just talking about strings a string_view. I was thinking shared_pointer because of the reference counting.
I had a rare case, where Box helped with performance and memory usage. Arc was not needed because all "users" of the object only needed &[T]. The Box is 8 bytes smaller than Vec, what can be very relevant sometimes. And it communicates: "does not change anymore", without needing a newtype for that.
This made me realize something. Vec is a usize longer because it contains the length (so it can double it's allocation when out of space). Box is mutable, but the length isn't (without reassigning). So it's like a constant-sized slice, but with a size that can be defined at runtime.
I really wonder now whether his representation of Arc is correct. If it is, then the current implementation is stupid. The Arc should only be a simple thin pointer. Each individual Arc has no need to duplicate the length. That should be stored on the heap only once. The pointer also should point directly to that length so that it can be fetched right away to check that your index is valid, then immediately followed by the string itself. The strong and weak counts need to be accessed less frequently ( only when cloning or dropping ) and so should be obtained by subtracting from the thin pointer. The lifetime of the str is the same as the thing you create it from. If it is created from a string literal, then it's lifetime is 'static, if it is not, then it is whatever that lifetime is. In this case, I would make my monster IDs 'static so you don't have to worry about lifetimes and can clone the str anywhere and everywhere cheaply.
&str is a pointer to a str, in the same way as &[T] is a pointer to [T], str is !Sized and is basically the same as [u8] just with more methods. I'm not talking about sized slices, [u8; N], i'm talking about unsized slices that you can't put on the stack, but you can Box or Arc them (usually with Vec::into_boxed_slice, String::into_boxed_str, String::into)
atomic incr and decr invalidates the cache so is more expensive than you'd think in certain cases. It's one of the original tricks in python to get rid of the GIL but single threaded performance was degraded to such an extent as to make it completely unviable (something like 30% overhead).
Hi Prime, so I'm not sure if anyone in chat answered, but I want to answer some of the questions anyway. 1.The weak and strong aren't a part of str, they're a part of (A)Rc, something like a (usize, usize, T) is allocated on a stack, since you're going to have to share the reference count between Rcs, and only drop the contents of the allocation when the destructor is called with 1 reference left. 2. The type str does not have a destructor, it is literally the characters themselves, a series of bytes, akin to [u8], the type does not have any allocation. And cannot live on a stack because the size of any str is not known, it can be of any length, which is why it always lives in the &str form(or any other pointers like Rc), as that has a known length of 2 pointers(standard for DSTs)
Why doesn't arc store the string length of the string with the reference count and data? This makes the strings use no more memory, and gives only one pointer in a struct. The only disadvantage I can see is that processors can't just read the number of bytes from the pointed to address at once, and need to first read the size from the pointed-to location, and then the data
2:43 memcpy is not O(1) so how is cloning an arc O(1)? memcpy is O(n) where n = number of bytes to copy. memcpy could be the fastest way to copy memory but that doesn't make it O(1). Is the number of bytes to copy constant regardless of the size of the arc or did the guy in the video make a mistake?
Atomics are extremely cheap, and most of the times on x86 you don't even need an explicit lock/atomic instruction since the usual instructions are inherently atomic. So the overhead of using atomics in precisely 0 in such cases (the assembly generated is identical, for data sizes up to 64 bits, and perhaps more if the hardware supports it). But on ARM and similar RISC architectures (basically all embedded systems), atomics do indeed have a measurable overhead of a few instructions. Fun fact, the reason why the M1 mac emulation is faster compared to windows on ARM (qualcomm), despite both being ARM processors is because apple implemented some of the memory guarantees of x86-64 processors and consequently do not pay that overhead.
25:58 I belted laughing. I laughed so hard I had to unbuckle. I'll have so hard My parents down the stairs are yelling. I laughed so hard that I can't breathe.
@@ThePrimeTimeagen On your fist try, the LSP says it can give you what you want, but there’s two options, Arc and Arc. You just had to choose one. By changing the whole statement into an assignment you told it which you wanted, but the previous one was totally fine too.
Folks saying "just use enum"; yes, if all monster types are known at compile time. Which might not be the case for a game e.g. you might want to load all monster definitions from a data file when the game starts up. Or maybe you even wanted to create monster types dynamically during gameplay. You also have to think about saving/loading game state.
A full minute of Prime just not reading the error XD "HOW DOES THIS WORK ITDOESN"TWORK!~" "type annotations required. multiple `impls` for Arc: From" rustc said calmly.
Common sense: I also do very similar in C/C++ just using char* + length (latter if I only need) instead of string for things that are not being mutated. Also even if I build thing with a string I just make it owned by something with clear ownership and store the data. Its basically for the same performance considerations - just a difference in ownership management. I also agree: they should have talked about using "RC" for this not ARC unless threading is involved. People start to involve threading TOO EARLY these days honestly and its code pessimization...
Is len really stored with the ptr and not with the str data? How would that be possible with a generic data structure like Arc that could hold anything, like something that hasn't a len.
Rust's pointer types automatically include length (or V-table) information whenever their pointee type is dynamically sized. And the compiler knows whether the pointee type is dynamically sized because it always keeps track of what specific type each generic placeholder refers to (unlike java for example which does type erasure on its generics).
No doubt it depends on your memory ordering requirements - relaxed might be equivalently as fast, SeqCst needs to ensure ordering between threads so is going to be slower. Is atomic Inc/dec going to be faster than a malloc/dealloc on a thread local arena allocator?
@@benharris4436 Depends on the scenario, there for sure are scenarios where atomic inc/dec can become and issue (Clone+drop in tight loop across multiple threads) but I would argue that these scenarios are rare.
I do not like Rust as a language, but the nuanced discussions around memory management brought about by Rustaceans are great. I've implemented several bespoke virtual machines for creating compact cryptographic proving systems, but they have not had anywhere nearly this much detail in their memory models.
@@OneWingedShark I don't mind functional style programming. I just don't like how there are a thousand different syntactical features, slow compile times, and constant circular firing squad nonsense. Maybe if I spent a couple hundred hours experimenting with it, I'd grow to appreciate (or at least become accustomed to) the strange syntax, but I don't have time for it right now.
@@k98killer That's a lot of why I recommended Ada: if you already know C, C++, Java or really anything from the Algol family you can pick it up and be somewhat productive *very* quickly -- the thing to keep in mind, however, is to use the type-system to model your problem.
I wrote this in another video about rust, about me writing a toy debugger - and this is EXACTLY what I meant. To construct these long lived Arc for instance, you will have to leak the life time in some fashion - to convince the BC that you know what you are doing. It might be handled by a library, you might never touch it, but at some point, it will exist in an arena somewhere and the life time will have been "leaked" so you can use Arc. I had some fools regurgitating "skill issue" at me. Arc is used, in the sort of same scenarios as you do string interning
Could you, in very simplified terms, say that a double pointer indirection is like javascript calling a method that calls a method that accesses a string and returns some of it? For like.. no reason.
Doesn't the derived Eq traits just compare the pointers to the strings? In that case you do not even need the ids to point to strings, it can point to anything. And why not just use int ids then?
@@mattiaslaserskold137 firstly, to address the technical element of this, to the point, it's a contrived example to simplify the explanation. your solution only solves the contrived example (keeping an ID of something). it doesn't solve the actual use cases that are solved by sharing immutable runtime state (e.g. Arc) with automatic memory management. you asked all of us a question 'why are you using X to do Y, when you could just use L to do Y', and I answered you with 'he's not really doing Y, he's really doing W, and he's just showing X because it's a way to solve for W' to your most recent comment, sorry you are so offended by having your question answered. let's just make it clear that I believe that you are a free man. if you feel that I am somehow restricting your right to be wrong on the internet, then that's a you problem.
@@Tigregalis "sorry you are so offended by having your question answered": In what world do you live in? First of all, you obviously did not even understand my question, otherwise you would have pointed out that when comparing Arcs (or Rcs) with == you compare the values and not the pointers (as I have recently found out, not thanks to your non-answer). And secondly, stop projecting your own insecurity onto others. Why would you react so hostile to a technical question and then assume I should be the one being offended? Is it so common people react to you being an a** that you just assume everybody around you is being offended. Grow up, or go back to the comment section on stack overflow where you seem to have come from.
I don't Rust, but from what I can tell, Arc is a thread-safer pointer, but this whole video is about using it with specifically immutable data. If it's always immutable, why Arc?
In Rust you need to have an Arc (or Arc) to mutate the T behind a shared reference across threads. In Rust (exclusive==mutable) != (shared==immutable). My point here is that these are orthogonal concerns to thread safety. Rust manages your memory for you, by simply dropping things at the end of the scope in which they were created (normally). But you want to be able to reference data/resources across scopes (and across threads), and/or pass and share ownership around your program. You can't just do this in Rust because of lifetime and borrowing rules, simply put: data/resource can't outlive the scope in which it's created, and neither can references to that data, and the owner of the data frees that data when the owner goes out of scope. If my use of "scope" here isn't clear, then you can consider that all statements and expressions between the curly braces {} as belonging to the same scope. Inner scopes can access data/resources in outer scopes. Without a heap, stack-allocated stuff can only reference things lower down in the stack (within the same scope). You sort of "get around" this by "lifting" the data onto the heap instead of the stack (then it's sort of in an outside scope). Then, instead of a reference (&T) you use some other pointer type to reference that heap data: - Box: single owner (frees memory when the pointer on the stack goes out of scope) - Rc: multiple owners within the same thread (reference counted, frees memory when the last owner pointer goes out of scope) - Arc: multiple owners across threads (atomically reference counted, frees memory when the last owner pointer across all threads goes out of scope)
Isn't the String.into() just Arc::from(String), so no lifetime is needed because it's moving the String into the Arc? I think you just wouldn't be able to use the String directly any more.
The &Option vs Option one is also great
I suppose Arcs maybe are more useful when you are hit by great floods?
correct, they implement Send + Sync + LiveFromFlood
Send + Sync + 'a
where:
'a: 'flood
@@ThePrimeTimeagenseems dangerous for an arc to implement sync!
@@andrewf8366 Arc withount Sync is Rc
😂😂😂😂😂
I'll admit I'm completely convinced of Arc over String, because of how common it is to need to clone a string.
I'm less convinced about Arc, since I almost never need to clone a vector
that is fair
Why not just borrow it always ?
why not use str then directly? cloning a str won't change the underlying data
I use Arc, btw
i use Rch btw
btw when Arc or Rc are created from String or &str, they initially clone the contents of the str. That's why we don't need to bother about lifetimes. They are just not involved in this.
I wonder if you could move out of the String to create a Str pointer. Maybe a deref
@@onebacon_String does implement Deref. You cannot use str raw, because it cannot be put on a stack, as it is unsized, but you can convert String into Box without cloning and operate on it like that
Also note that if you have a bunch of short string ids, a crate like smol_str is a much better solution than this, because its strings are stack-allocated.
beautiful
Ah yes. The Rust way, include a new string for everything.
c++ std string does that automatically for small strings. C++ won.
@@notuxnobux Let's not go that far...
compact_str, it's faster, has mutation API and slightly bigger (24b instead 23b) strings.
Arc also implements From.
26:05 may be the best moment in all of Primeagen history.
That incredulous pause was hilarious!
Nobody mentioned the difference between Rc vs just &str. For most situations, a simple &str is simpler, but can make some lifetime annotation problems.
it's basically try using them in this order &str -> Box -> Rc -> Arc
impl From for Arc
calls into
impl From for Arc
Which calls into
::from_slice(v)
Which calls into
impl ArcFromSlice for Arc
which calls into
Arc::copy_from_slice
which calls into
Arc::allocate_for_slice
which then calls
ptr::copy_nonoverlapping
So it is taking the string bytes that the &str points to and copying them into the ArcInner allocation, removing the meaning of the lifetime. The ArcInner allocation is managed/owned by the Drop implementation of Arc.
bro provided a stack trace 👏
@7:40 I assume it's figured out at some point but a &str is a reference to a string. A str is a sequence of characters. A String owns a str, which contains the actual data. Arc owns the str, it's not Arc.
@20:10 This probably doesn't make a noticeable difference, but technically you could have the ptr in each stack instance of the Arc point directly to the data. Then you do a -2*word to access the strong field and -word to access the weak field, because you access the reference count fields less frequently than the data.
@22:50 I believe Arc::from(String) works.
@26:30 Yeah okay, it does. It copies the str, I believe, since it needs to prepend the refcount fields. A str is not a &str. A [T] is not a &[T].
@29:10 If you're using Arc in a single-threaded program linked against a malloc implementation that isn't thread safe, then if you get extremely lucky, maybe malloc will be faster than an atomic operation. However. If you're using Arc in a multi-threaded program (otherwise, why not use Rc?)... well, it has to be linked against a thread-safe malloc. And a thread-safe malloc needs to acquire locks or at the very least use atomics for double check locking. Thus, your two cases are
- you're using an Rc and it's single-threaded: then it's just going to be strictly faster, because incrementing a word is always going to be faster than any non-inlined function call, not speaking of a call to malloc.
- you're using an Arc and it's multi-threaded: then it's just going to be strictly faster again, because atomically incrementing a word is never going to be slower than whatever malloc has to do to ensure thread safety, and then malloc also has to other work.
For dropping, it's going to be the same. I believe most modern malloc implementations do most of their house-keeping on malloc calls and do rather minimal work on free calls, but the free calls still have to be thread-safe if you're using threads and linked against a thread-safe malloc.
Your example about sets vs arrays is a reasonable thing to be cautious about, but in this case, you have to remember that an atomic increment, while potentially expensive compared to a normal one, is the cheapest possible form of thread-safe operation, which means that anything thread-safe (like the version of malloc you have linked in any program using multiple threads) is going to be no faster than Arc.
@31:30 I think the thesis of his video is exactly that it is that its telos is to be mutated. If it's reached the point where it's never going to be mutated, but is still sticking around for a while, then you convert it into an Rc/Arc from that point on.
Atomic increments and decrements are EXTREMELY cheap. Not as cheap as a normal increment, but the difference is negligible until false sharing comes into play, but at that point you're already doing thousands of atomic operations on the same integer per second, which is a really unlikely scenario in this case. Either way, it's going to be orders of magnitude faster than having at least two function calls (malloc and memcpy*) plus the time required to actually allocate and copy the memory over.
Edit since people are pointing out a couple of oversights:
- memcpy is not a syscall, but still a function call unless inlined
- malloc is also technically not a syscall but will likely involve one at some point, unless your allocator already has some memory ready for use.
Malloc and memcpy are not syscalls, and allocating memory from the system is done in amortized fashion.
I'm not trying to say that that allocating on heap will be generally faster, but for sure you can design a multithreaded scenario where the memory allocator can use thread local alloc impl and be faster than Arc.
Arc will most likely be much faster in most cases, but there are exceptions for sure.
And rust is using jemalloc as the allocator, so there are thread local cached allocations. So you are now comparing global atomic ops with thread-local ops.
@@benharris4436 Rust switched to the platform's native allocator (glibc on GNU+Linux) a while ago.
@@benharris4436 Rust no longer uses jemalloc by default by the end of 2018, now it usually uses whatever the system libc provides (though one can manually enable jemalloc, this is not always possible on all targets).
@@HarryChengv2 I have to say, it is funny that by default, 99% of rust programs are dynamically linked to glibc. yes, you can build it statically or change memory allocators, or even write your own, but it's funny nonetheless.
I think Arc/Arc does not need the original lifetime because it copies the contents of the slice. It's an owned type.
See implementation: rust std src/alloc/sync.rs.html#1467 (seems I cannot post the link to docs?)
Here is the extract:
```
#[cfg(not(no_global_oom_handling))]
impl ArcFromSlice for Arc {
#[inline]
default fn from_slice(v: &[T]) -> Self {
unsafe { Self::from_iter_exact(v.iter().cloned(), v.len()) }
}
}
```
Also his visualization is wrong, Arc doesn't have `len` and only contain a single pointer.
32:22, yes they're not mutable, but that's exactly the issue, String is the same type wether binded to a mutable or immutable variable, but it's inner structure is mutable. if you have an immutable String you're paying the same cost as you would if it was mutable.
a better comparisson to String, rather than Arc, is Box, which imposes the same ownership semantics (single owner), but doesn't have the cost of being mutable (or the cost of being thread-safe)
I think it's really confusing that this guy says Arc is better, it's only better if you need multiple ownership and thread capacities. if you won't send it between threads, Rc is better, and if you don't need multiple ownership, Box is better
1:55 That sexy animation looks to me like it's done with manim, the animation library originally developed by 3blue1brown for his math videos, later forked into a community project. Apart from some boilerplate like imports and wrapping things in the correct subclass, making that animation is as easy as
t=Text("Some smexy text")
play(Write(t))
Logan Smith does some really nice mid level rust videos. There is a lot of entry level stuff so it's a nice change of pace
who makes entry lvl rust videos, I wanna learn
That was my first impression of Vec and String. That there is no real point of using them unless they're mutable. A different structure would be better in most immutable cases. And even when dealing with things greater than stack size you can use Box instead of Vec.
It's a bit annoying that ToOwned uses Vec and String though.
Is there a way to make a Box of a specific size like the vec! macro?
@@rosehogenson1398
let t: Box = Box::new([value; len]);
only works if size is known at compile time.
let t: Box = vec![value; len].into();
works without reallocation (Vec has the exact capacity we want) but it's not specified explicitly.
Just started rust, had the ability to clone or pass by reference and clone really felt dirty. I feels at 2:00. The Rust Book is really well put together and I'm enjoying the language for the most part (compiler is super helpful). I just hope internal drama doesn't kill the language.
Arc and Rc are what you should be using instead of cloning all over the place. There is a reason string types in most languages do not own their data.
str is just utf8 compliant [u8]
you make a string somewhere, and then you can move its data to make the first Rc/Arc instance ever (idk if its .into() or whatever actually - edit: watched video and saw it is in fact into()), and that will never dealloc until all instances die. so the lifetime is effectively 'static (i dont remember if thats actually the case... actually i think if you pull a reference from an &'a Rc its going to be Rc's lifetime 'a, but inside of Rc its 'static), since if you can access that str, that means you have an instance of an Rc/Arc, and that will not die on you because theres an instance
Rc: 'static iff T: 'static, so you could say it is the case
My question is: why do you need to clone the Arc (or Rc) vs taking a shared reference? (Especially for immutable data see: 1:44). I'm sure that these are situations where you want the clone, but I feel like the fair point of comparison is taking a shared reference rather than cloning the whole vector/string. I guess the last few seconds with the Box he started to get into it, but I feel like the video wasn't super well structured.
If you have a string in one of your struct fields, and want other fields to point to this string, because you don't want to have unnecessary copies, eg. you have a field `HashMap`, and then want to build indexes into the main hashmap, so `HashMap`, and then also maybe you want values inside the hashmap to be able to refer to other values by using a hashmap key, etc. then using `Arc` as `MyStringId` allows you to do that without copying the string every time. And you can't use references because safe Rust doesn't allow self-referential structs. You're still free to return `&str` from the public API and I think that's preferred instead of exposing `Arc`s, which are an implementation detail.
@@Bravo-oo9vd the right solution, in your example, is to allocate a String, which is not owned by the main map. The main map should be of type HashMap. Both &str's are sub slices of the originally allocated String. This video represents a misunderstanding over the purpose of the types. Reference counting and atomics have nothing to do with the problem at hand.
Note: Cloning an Arc will *always* be faster than cloning a Vec or String, because Rust doesn't have small String/Vec optimizations (there are good reasons for that), so there will always be a heap allocation in the String/Vec version, while the relevant parts of the Arc clone (the fat pointer) are stack-allocated (or directly in registers). And the atomic memory access _is_ slow but operationally insignificant compared to malloc (or similar).
Also, for the record, a _str_ is not a string slice but a bunch of (valid) unicode bytes with unspecified length. A _&str_ is a girthy pointer to that block. That is why it's not Arc but Arc, the Arc taking the place of the fat pointer, having an allocation of the unicode bytes as its data.
Yerp, you'll find info about str, and friends (like [T], and dyn Trait) under dynamically sized types (DST). DSTs along with zero-sized types (ZST) (looking at them `struct Empty;` decls, and unit maybe?) are what make allocating in Rust so colorful, but give so much power to the language imo.
29:40 The allocator is a shared resource between all threads. So I would assume any allocation/deallocation includes a atomic increment/decrement anyway.
Here's how you do it:
let a: Arc = Arc::from("a");
println!("{}", a); //prints: a
It's not possible to create a variable of type str directly, because it doesn't implement Sized. But it's posdible to have a pointer to it. &str is one such pointer. It's value is not a string of characters, but instead it's a pointer to the beginning of the string of characters and the length of them. So actual size of &str variable would be double usize.
But apart from this pointer type, it's possible to hold str in any pointer: Box, Rc etc.
To create such variable, you commonly use a method from, to which you can pass a reference (you can convert a reference to smart pointer). In this case, any string literal is actually a static lifetime reference to a string slice (str) stored inside binary. So you can do it directly, like this Arc::from("string slice"), where "string slice" is of type &str.
You'd know this, by reading the Book.
The same goes for any slices, but it becomes a lot more obvious in those cases, because they don't use special annotation, like str and instead actually use [T] syntax.
You can also take a rawpointer to &str, but you'd need it's length separately, if you want to do anything remotely useful with it:
let s = "slice";
let len = s.len();
let p = s as *const str; // immutable
let p = s as *mut str; // mutable
Arc has a major disadvantage against String: it doesn't implement Serialize/Deserialize
21:23
@@norude thanks, missed that
serde's rc feature will do that
I think one major problem/downside of using these arcs is that it makes your code a lot less straight forward/readable. Everyone knows what a Vec stands for, but an Arc seems a lot more foreign.
I think optimizations like these can be very helpful in certain situations, but I think that readability is more important in most use cases. Especially since it might only save 1 ms and a couple of bytes on calls that take 100s of ms on modern machines with gigabytes in RAM.
What do you think Arc is? What do you think a random reader would think it is?
It might stump you the first time you see it. If the use of Box (which is even better than Arc) is ubiquitous this is fine.
Let foo = String::new("Hello") ;
Let arc_foo: Arc = Arc::from(foo) ;
Arc::from takes ownership of the String so now the arc manages the memory of the str.
Names to avoid in dev videos: (or overuse, one of those)
- Goblin
- Candice
- Phil
- Fitness
- Luke
I have recently taken a look at the Rust compiler and you could run into similar cloning conundums, when doing type inference/coerscion. They solved it by using a 'tcx lifetime as arena allocation. Seeing lifetimes used as a marker for memory arenas has given me a new way of looking at lifetimes.
This is not dirty. Wait until you try Rc.
Wasn’t pre-C++11 std::string effectively the equivalent of `Rc` (+ the logic for resizing the buffer) and then it was changed to be the equivalent of `Box` for performance reason, doing the exact opposite of what is presented here? Also, if all you have are read-only strings, why do you need an `Arc/Rc` and not just a reference `&str` ?
A str itself is a sequence of bytes, basically a char[0] in C, it is not a pointer but rather a variable char array.
Rc is the same as &str except that it lives until there are no more references left
It is a sequence of char, but not of bytes. Strings in Rust are UTF-32.
@@AlexisPaqueswait, isn't utf-32, 4 bytes fixed sized? Why prefer that over utf-8? On a Hella lot of strings that'll be a huge memory hog, especially when almost all strings are ASCII anyway, unless localized
I can see a speed benefit though, as it requires less instructions
@@AlexisPaques Strings in Rust are always encoded in UTF-8 and a String is literally a Vec under the hood. A str is comparable to a [u8] with the difference being additional functions and ensured UTF-8.
@@AlexisPaquescompletely wrong, sqrry is correct
str is just a compilation guarantee that the underlying [u8] is in valid utf-8 format, thats why as_bytes is const, because it doesnt change anything, it just gives you raw access to the &[u8] the Arc (or Box, or String) is pointing to
26:09 this is what's happening.
let foo = String::from("hello world");
let temp: &str = foo.deref();
let foo: Arc = temp.into();
rust is kind enough to call the .deref() method for you.
21:44 There's an educe crate that implements all these for you. You're welcome.
Also, &'static str is the king of strings. If you *really* want to get strings from runtime and care a lot about performance, you can consider a non-static &'a str. However, I acknowledge that it poisons your code with
25:17 Arc vs Vecwas the initial thought, no?
Best part of waking up, is understanding nothing Prime says about low level programming.
So basically if you turn a String into a str you are moving ownership (and the string memory) to a str data type (which is just an array of bytes) and making it immutable. This means that String is dropped and memory is moved to a str. If you wrap a str in an Arc you basically have the same thing as a &str, but lifetimes are handled at runtime and the owner of the string bytes is the Arc (really there is no owner, the memory is leaked with Box.leak and deleted when the Arc no longer has references). When you drop the Arc the str bytes are dropped too.
21:29 I wanted the derive that derives all the crap, but after finding out that the common derives are hardcoded into rustc and are thus fast, I immediately gave up on having a proc macro.
Another thing to consider is that atomics on x86 and x64 are free on pointer sized values. In fact, regular writes are atomic as well. This means that any slowdown is mainly due to the compiler not being allowed to reorder atomic loads and stores. (And possibly weak references being more complicated in threaded settings.)
Doesn't the Arc have to copy the data to it's own heap memory?
It does seem that way. Like he explained in the video, Arc is just a pointer to the ArcInner type, which is a strong and weak count followed by the actual data.
No if you clone an arc they both point to the same arc inner
And the reason you have a 16 byte pointer object for [T] is because of Sized. An Arc is just 8 bytes and an Arc is 16, for example.
Rust has taught me so much about memory.
@@marlonrondinelli903 yes, that is what I said...
@@marlonrondinelli903 i am specifically talking about when you make an arc from a string or a &str
This was super interesting and in my expperience is true whenever applications get middle to large size.
Memory & memory-cache can be huge deals and as such having flexible data structures can be a absolutely necessary
This reminds me a lot of Span vs String/Array in C# in a few years back
Why not use &Vec instead?
Vec and String is for mutating. The underlying data is mutable, so as long as you own them (e.g. don't pass &Vec or &String, which you should never do anyways) then you can simply access the inherent mutability of the type by "moving" it: 'let deez_nuts = String::from("goblin"); let mut deez_nuts = deez_nuts;'
that's not moving, that's a copy.
@@Turalcar No, String does not implement the Copy trait. This is simple Rust move semantics.
@@throwaway3227 I meant String::from("goblin") copies from "goblin" (or clones, whatever). Tbh, the entire sentence was hard to parse.
@@Turalcar Yes, 'String::from' will copy to the heap, but that's not relevant to my statement. For String (however you chose to construct it) the underlying data on the heap is mutable.
Isn't a c style null terminated string still more efficient and less indirection than a Box?
A null terminated string pointer is only half the size of a Box (but the heap allocation for it is 1 byte larger). The amount of indirection is the same for both (= 1). But the null terminated string also has 2 disadvantages:
- it can't contain the null character
- you must compute the length every time you need it at the cost of O(N). Whereas Box includes the length directly (that's why it's twice as large).
More memory efficient, sure, but less computationally efficient (in case you need to know length of it, which you do pretty often). It's also more error prone (you can just corrupt the nul termination character and you break everything, which isn't that hard in C, since you can randomly access memory and dereference it in C).
0:10 I was really confused by Logan's video. I'm really excited to hear your take.
If the involved lifetimes are simple enough you can also just use &str. You can use &str as keys in a map as long as the lifetime of the map is guaranteed to be shorter than of what wherever the &str comes from.
Thinking about memory makes me feel calm
So this is like shared_pointer from like C++ or something. Or if you were just talking about strings a string_view. I was thinking shared_pointer because of the reference counting.
I had a rare case, where Box helped with performance and memory usage. Arc was not needed because all "users" of the object only needed &[T].
The Box is 8 bytes smaller than Vec, what can be very relevant sometimes. And it communicates: "does not change anymore", without needing a newtype for that.
This made me realize something. Vec is a usize longer because it contains the length (so it can double it's allocation when out of space). Box is mutable, but the length isn't (without reassigning). So it's like a constant-sized slice, but with a size that can be defined at runtime.
If you're wondering about how he did that nice animation, he's using the Manim library by the math youtuber 3blue1brown
I really wonder now whether his representation of Arc is correct. If it is, then the current implementation is stupid. The Arc should only be a simple thin pointer. Each individual Arc has no need to duplicate the length. That should be stored on the heap only once. The pointer also should point directly to that length so that it can be fetched right away to check that your index is valid, then immediately followed by the string itself. The strong and weak counts need to be accessed less frequently ( only when cloning or dropping ) and so should be obtained by subtracting from the thin pointer. The lifetime of the str is the same as the thing you create it from. If it is created from a string literal, then it's lifetime is 'static, if it is not, then it is whatever that lifetime is. In this case, I would make my monster IDs 'static so you don't have to worry about lifetimes and can clone the str anywhere and everywhere cheaply.
&str is a pointer to a str, in the same way as &[T] is a pointer to [T], str is !Sized and is basically the same as [u8] just with more methods. I'm not talking about sized slices, [u8; N], i'm talking about unsized slices that you can't put on the stack, but you can Box or Arc them (usually with Vec::into_boxed_slice, String::into_boxed_str, String::into)
atomic incr and decr invalidates the cache so is more expensive than you'd think in certain cases.
It's one of the original tricks in python to get rid of the GIL but single threaded performance was degraded to such an extent as to make it completely unviable (something like 30% overhead).
does Rust have small string optimization like C++?
See compact_str crate
So Arc is just shared_ptr but a little better
I just want to say:
> Just Let The Man Cook and Shut Up!
> Arc
Hi Prime, so I'm not sure if anyone in chat answered, but I want to answer some of the questions anyway.
1.The weak and strong aren't a part of str, they're a part of (A)Rc, something like a (usize, usize, T) is allocated on a stack, since you're going to have to share the reference count between Rcs, and only drop the contents of the allocation when the destructor is called with 1 reference left.
2. The type str does not have a destructor, it is literally the characters themselves, a series of bytes, akin to [u8], the type does not have any allocation. And cannot live on a stack because the size of any str is not known, it can be of any length, which is why it always lives in the &str form(or any other pointers like Rc), as that has a known length of 2 pointers(standard for DSTs)
it's 2 thirds because Vec/String uses 24 bytes where Arc str uses 16 bytes, he said that at the beginning lol
Why doesn't arc store the string length of the string with the reference count and data? This makes the strings use no more memory, and gives only one pointer in a struct.
The only disadvantage I can see is that processors can't just read the number of bytes from the pointed to address at once, and need to first read the size from the pointed-to location, and then the data
Any examples of comparative tests out there?
2:43 memcpy is not O(1) so how is cloning an arc O(1)? memcpy is O(n) where n = number of bytes to copy. memcpy could be the fastest way to copy memory but that doesn't make it O(1). Is the number of bytes to copy constant regardless of the size of the arc or did the guy in the video make a mistake?
It’s constant because it’s cloning Arc, which is a constant size.
(2 counters and a pointer)
@@avalsch thanks. That makes sense.
gosh_nejeb at 11:37 - YES. FIRESHIP ALWAYS GETS A PASS.
Atomics are extremely cheap, and most of the times on x86 you don't even need an explicit lock/atomic instruction since the usual instructions are inherently atomic. So the overhead of using atomics in precisely 0 in such cases (the assembly generated is identical, for data sizes up to 64 bits, and perhaps more if the hardware supports it).
But on ARM and similar RISC architectures (basically all embedded systems), atomics do indeed have a measurable overhead of a few instructions.
Fun fact, the reason why the M1 mac emulation is faster compared to windows on ARM (qualcomm), despite both being ARM processors is because apple implemented some of the memory guarantees of x86-64 processors and consequently do not pay that overhead.
Finally papa Prime is discovering Logan Smith, here is where it gets JUICY
19:30 It's very much like a hard link in *nix CLI.
25:58 I belted laughing. I laughed so hard I had to unbuckle. I'll have so hard My parents down the stairs are yelling. I laughed so hard that I can't breathe.
Dear lord IT SAYS TYPE ANNOTATION NEEDED. Just annotate it. I thought he was experienced in Rust?!
i am unsure what you are capping about
@@ThePrimeTimeagen About the fact that if you're unsure what to do next, trust the compiler! It knows. It knoooows!
@@ThePrimeTimeagen On your fist try, the LSP says it can give you what you want, but there’s two options, Arc and Arc. You just had to choose one.
By changing the whole statement into an assignment you told it which you wanted, but the previous one was totally fine too.
Folks saying "just use enum"; yes, if all monster types are known at compile time. Which might not be the case for a game e.g. you might want to load all monster definitions from a data file when the game starts up. Or maybe you even wanted to create monster types dynamically during gameplay. You also have to think about saving/loading game state.
type Str = Arc;
A full minute of Prime just not reading the error XD
"HOW DOES THIS WORK ITDOESN"TWORK!~"
"type annotations required. multiple `impls` for Arc: From" rustc said calmly.
Common sense: I also do very similar in C/C++ just using char* + length (latter if I only need) instead of string for things that are not being mutated. Also even if I build thing with a string I just make it owned by something with clear ownership and store the data. Its basically for the same performance considerations - just a difference in ownership management.
I also agree: they should have talked about using "RC" for this not ARC unless threading is involved. People start to involve threading TOO EARLY these days honestly and its code pessimization...
Fantastic video! Thanks for sharing it!
hey prime, can you upload that chat js client? i really want to dive into the code a bit more
I was getting so mad, when Rust tells you it needs type annotations just annotate the fricking types. Just use `Arc::::from::` goddamnit 😂😭
6:16 arc sounds like an extension of rc
Span
The only context in which you can say "it's okay to be wide".
Is len really stored with the ptr and not with the str data? How would that be possible with a generic data structure like Arc that could hold anything, like something that hasn't a len.
Rust's pointer types automatically include length (or V-table) information whenever their pointee type is dynamically sized. And the compiler knows whether the pointee type is dynamically sized because it always keeps track of what specific type each generic placeholder refers to (unlike java for example which does type erasure on its generics).
Atomic is harder to use, but is as fast as a normal value on x86_64, on ARM can be more expensive!
that is interesting...how is that possible?
I’d like to see the measurements for that.
No doubt it depends on your memory ordering requirements - relaxed might be equivalently as fast, SeqCst needs to ensure ordering between threads so is going to be slower. Is atomic Inc/dec going to be faster than a malloc/dealloc on a thread local arena allocator?
@@benharris4436 Depends on the scenario, there for sure are scenarios where atomic inc/dec can become and issue (Clone+drop in tight loop across multiple threads) but I would argue that these scenarios are rare.
I do not like Rust as a language, but the nuanced discussions around memory management brought about by Rustaceans are great. I've implemented several bespoke virtual machines for creating compact cryptographic proving systems, but they have not had anywhere nearly this much detail in their memory models.
Maybe try Ada? It's more in the procedural/oop camp than the functional, but it's also concerned with being correct/safe.
@@OneWingedShark I don't mind functional style programming. I just don't like how there are a thousand different syntactical features, slow compile times, and constant circular firing squad nonsense. Maybe if I spent a couple hundred hours experimenting with it, I'd grow to appreciate (or at least become accustomed to) the strange syntax, but I don't have time for it right now.
@@k98killer That's a lot of why I recommended Ada: if you already know C, C++, Java or really anything from the Algol family you can pick it up and be somewhat productive *very* quickly -- the thing to keep in mind, however, is to use the type-system to model your problem.
It's two thirds of the pointer cost... But the real question is, what's a piecost?
Char vs car is ambiguous if you ever lisp. So I don't mind the inconsistency.
But if you're getting pedantically consistent, then it should be a "care". I don't personally say "car-ichter"
I wrote this in another video about rust, about me writing a toy debugger - and this is EXACTLY what I meant. To construct these long lived Arc for instance, you will have to leak the life time in some fashion - to convince the BC that you know what you are doing. It might be handled by a library, you might never touch it, but at some point, it will exist in an arena somewhere and the life time will have been "leaked" so you can use Arc. I had some fools regurgitating "skill issue" at me.
Arc is used, in the sort of same scenarios as you do string interning
7:33 no you're wrong. str is not a reference to a string.
7:17 Internet comment Etiquette with Erik : Querious/Curious/Delirious Betsy
This is why Rust is my favorite language.
Could you, in very simplified terms, say that a double pointer indirection is like javascript calling a method that calls a method that accesses a string and returns some of it? For like.. no reason.
"I don't know if its TELOS is to be mutated" 🤣😂
Doesn't the derived Eq traits just compare the pointers to the strings? In that case you do not even need the ids to point to strings, it can point to anything. And why not just use int ids then?
The IDs were not the point of the video...
@@Tigregalis Do you mean that you are only allowed to comment on one single thing? And if the thing does not work, it does not work.
@@mattiaslaserskold137
firstly, to address the technical element of this, to the point, it's a contrived example to simplify the explanation. your solution only solves the contrived example (keeping an ID of something). it doesn't solve the actual use cases that are solved by sharing immutable runtime state (e.g. Arc) with automatic memory management.
you asked all of us a question 'why are you using X to do Y, when you could just use L to do Y', and I answered you with 'he's not really doing Y, he's really doing W, and he's just showing X because it's a way to solve for W'
to your most recent comment, sorry you are so offended by having your question answered. let's just make it clear that I believe that you are a free man. if you feel that I am somehow restricting your right to be wrong on the internet, then that's a you problem.
@@Tigregalis "sorry you are so offended by having your question answered": In what world do you live in?
First of all, you obviously did not even understand my question, otherwise you would have pointed out that when comparing Arcs (or Rcs) with == you compare the values and not the pointers (as I have recently found out, not thanks to your non-answer).
And secondly, stop projecting your own insecurity onto others. Why would you react so hostile to a technical question and then assume I should be the one being offended? Is it so common people react to you being an a** that you just assume everybody around you is being offended. Grow up, or go back to the comment section on stack overflow where you seem to have come from.
Chat is having fun with the interesting Unicode characters 👍
Not finished watching, I think Arc is OK because it's read-only value. Correct?
Logan Smith has become one of my favourite youtubers
I think the ref count terminology should be changed from "strong/weak" to "hard/soft".
I don't Rust, but from what I can tell, Arc is a thread-safer pointer, but this whole video is about using it with specifically immutable data. If it's always immutable, why Arc?
In Rust you need to have an Arc (or Arc) to mutate the T behind a shared reference across threads. In Rust (exclusive==mutable) != (shared==immutable). My point here is that these are orthogonal concerns to thread safety.
Rust manages your memory for you, by simply dropping things at the end of the scope in which they were created (normally).
But you want to be able to reference data/resources across scopes (and across threads), and/or pass and share ownership around your program.
You can't just do this in Rust because of lifetime and borrowing rules, simply put: data/resource can't outlive the scope in which it's created, and neither can references to that data, and the owner of the data frees that data when the owner goes out of scope.
If my use of "scope" here isn't clear, then you can consider that all statements and expressions between the curly braces {} as belonging to the same scope. Inner scopes can access data/resources in outer scopes.
Without a heap, stack-allocated stuff can only reference things lower down in the stack (within the same scope).
You sort of "get around" this by "lifting" the data onto the heap instead of the stack (then it's sort of in an outside scope).
Then, instead of a reference (&T) you use some other pointer type to reference that heap data:
- Box: single owner (frees memory when the pointer on the stack goes out of scope)
- Rc: multiple owners within the same thread (reference counted, frees memory when the last owner pointer goes out of scope)
- Arc: multiple owners across threads (atomically reference counted, frees memory when the last owner pointer across all threads goes out of scope)
Isn't the String.into() just Arc::from(String), so no lifetime is needed because it's moving the String into the Arc? I think you just wouldn't be able to use the String directly any more.
Isn't Arc a distribution of a Linux OS?
Now I know: I am untalented. And knowing is half the battle...
Destroying the same monster again and again, you are the real monster.
Isn't this basically the flyweight pattern?