What's the computational complexity of "HashSet::len"? - rust

The document says that get and insert for HashMap (not HashSet) are Ο(1)-like, but not for HashSet or len.
What is the computational complexity of HashSet::len?
Usually, the computational complexity of len is Ο(1). Is there a statement that indicates this?
https://doc.rust-lang.org/stable/std/collections/index.html#maps

Had to go down a bit of a rabbit hole for this one. In short, reading the source code from std::collections::HashMap indicates that the standard HashMap inherits its len() functionality from the crate hashbrown, which is a Rust port of the Google HashTable variant, SwissTable (github). Tracking down the implementation of len() in this crate leads down to the underlying class, RawTable, which contains an instance of RawTableInner<A>, generic for the entry type A. This struct contains a slot called items : usize which is returned whenever len() is called. This indicates that the len() function simply returns the value of an internally stored integer count which keeps track of the number of entries.
Overall, this suggests that the time complexity of len() will be O(1), as it isn't doing any iteration or counting, but rather simply return the value of an entry counter which has been maintained over the course of the HashTable's construction.

Related

What's the most efficient: a reference to an ArrayBase or an ArrayView?

I'm doing improvement on a Rust codebase that uses the ndarray crate to manipulate arrays. I have one question I could not find an explicit answer in the documentation.
Is it more efficient to pass an instance of ArrayView as an argument to a function or should I use a reference to an Array instead? My intuition is that since ArrayView is a view of an array, when doing computations, it only passes a view of the array and does not grant ownership to the function (hence does not copy) the underlying data.
In short, is there any speed gain to expect from switching from passing instances of ArrayView to passing references of Array?
My goal is to avoid useless memory allocation/duplication which can be very costly when dealing with large arrays.
ArrayBase is a generic struct that can act as both an ArrayView and an Array, so I assume you mean a reference to the owned data, i.e. an Array.
Neither version will clone the array, so they should be approximately equally efficient. You can always benchmark to verify this.
As I see it, the difference is mostly that ArrayView will make the function more flexible – you can pass in parts of larger arrays, or an ArrayView created from a slice, whereas the variant that takes a reference to Array can only be called when you really have an Array of the desired size.

Temporary materialization conversion - Confusion about terminology and concepts

Hi stackoverflow community,
I'm a few months into C++ and recently I've been trying to grasp the concepts revolving around the "new" value categories, move semantics, and especially temporary materialization.
First of all, it's not straightforward to me how to interpret the term "temporary materialization conversion". The conversion part is clear to me (prvalue -> xvalue). But how exactly is a "temporary" defined in this context? I used to think that temporaries were unnamed objects that only exist - from a language point of view - until the last step in the evaluation of the expression they were created in. But this conception doesn't seem to match what temporaries actually seem to be in the broader context of temporary materialization, the new value categories, etc.
The lack of clarity about the term "temporary" results in me not being able to tell if a "temporary materialization" is a temporary that gets materialized or a materialization that is temporary. I assume it's the former, but I'm not sure. Also: Is the term temporary only used for class types?
This directly brings me to the next point of confusion: What roles do prvalues and xvalues play regarding temporaries? Suppose I have an rvalue expression that needs to be evaluated in such a way that it has to be converted into an xvalue, e.g. by performing member access.
What will exactly happen? Is the the prvalue something that is actually existent (in memory or elsewhere) and is the prvalue already the temporary? Now, the "temporary materialization conversion" described as "A prvalue of any complete type T can be converted to an xvalue of the same type T. This conversion initializes a temporary object of type T from the prvalue by evaluating the prvalue with the temporary object as its result object, and produces an xvalue denoting the temporary object" at cppreference.com (https://en.cppreference.com/w/cpp/language/implicit_conversion) converts the prvalue into an xvalue. This extract makes me think that a prvalue is something that is not existent anywhere in memory or a register up until it gets "materialized" by such conversion. (Also, I'm not sure if a temporary object is the same as a temporary.) So, as far as I understand, this conversion is done by the evaluation of the prvalue expression which has "real" object as a result. This object is then REPRESENTED (= denoted?) by the xvalue expression. What happens in memory? Where has the rvalue been, where is the xvalue now?
My next question is more specific question about a certain part of temporary materialization. In the talk "Understanding value categories in C++" by Kris van Rens on YouTube (https://www.youtube.com/watch?v=liAnuOfc66o&t=3576s) at ~56:30 he shows this slide:
Based on what cppreference.com says about temporary materialization numbers 1 and 2 are clear cases (1: member access on a class pravlue, 2: binding a reference to a prvalue (as in the std::string +operator).
I'm not too sure about number 3, though. Cppreference says: "Note that temporary materialization does not occur when initializing an object from a prvalue of the same type (by direct-initialization or copy-initialization): such object is initialized directly from the initializer. This ensures "guaranteed copy elision"."
The +operator returns a prvalue. Now, this prvalue of type std::string is used to initialize an auto (which should resolve to std::string as well) variable. This sounds like the case that is discussed in the prior cppreference excerpt. So does temporary materialization really occur here? And what happens to the objects (1 and 2) that were "denoted" by the xvalue expressions in between? When do they get destroyed? And if the +operator is returning an prvalue, does it even "exist" somewhere? And how is the object auto x " initialized directly from the initializer" (the prvalue) if the prvalue is not even a real (materialized?) object?
In the talk "Nothing is better than copy or move - Roger Orr [ACCU 2018]" on YouTube (https://www.youtube.com/watch?v=-dc5vqt2tgA&t=2557s) at ~ 40:00 there is nearly the same example:
This slide even says that temporary materialization occurs when initializing a variable which clearly contradicts the exception from cppference from above. So what's true?
As you can see, I'm pretty confused about this whole topic. For me, it's especially hard to grasp these concepts as I cannot find any clear definitions of various term that are used in a uniform way online. I'd appreciate any help a lot!
Best regards,
Ruperrrt
TL;DR: What is a temporary in the temporary materialization conversion context? Does that mean that a temporary gets materialized or that it is materialization that is temporary? Also temporary = temporary object?
In the slides, is 3 (first slide) respectively 1 (second slide) really a point where temporary materialization occurs (conflicts with what cppreference says about initialization from pravlues of the same type)?
107 views, 6 months and no answer nor comments. Interesting. Here's my take on your question.
Temporary materialization should mean "temporary that gets materialized". I don't even know what "materialization that is temporary" would even mean to be honest.
The term temporary is not only used for class types.
Prvalues, loosely speaking, don't exist in memory unlike xvalues. The thing you should care about is the context. Let's say you have defined a structure
struct S { int m; };.
In the expression S x = S();, subexpression S() denotes a prvalue. The compiler with treat it just as if you have written S x{}; (note that I've put curly brackets on purpose because S x(); is actually a declaration of a function). On the other hand in expression like int i = S().m;, subexpression S() is a prvalue that will be converted to xvalue that is, S() will denote something that will exist in the memory.
Regarding your second question, the thing you need to know about is that with the C++-17, the circumstances in which temporaries are going to be created were brought down to minimum (cppreference describes it very well). However, the expression
auto x = std::string("Guaca") + std::string("mole").c_str();
will require two temporary objects to be created before assignment. Firstly, you are doing member access with c_str() method so a temporary std::string will be created. Secondly, the operator + will bind one a reference to the std::string("Guaca") (new temporary). and one to the result object of c_str(), but without creating additional temporary because of:. That's pretty much it. It's worth to note that the order of creation temporary objects isn't known - it's totally implementation-defined.
After that, we're calling the operator + which probably constructs another std::string which technically isn't a temporary object because that's a part of the implementation. That object might or might not be constructed into the memory location of x depending on the NRVO. In any case, whatever value does the prvalue expression std::string("Guaca") + std::string("mole").c_str() denote will be the same value (of the same object) denoted by the expression x because of cpp.ref:
Note that temporary materialization does not occur when initializing an object from a prvalue of the same type (by direct-initialization or copy-initialization): such object is initialized directly from the initializer. This ensures "guaranteed copy elision".
This quote isn't really precise and might confuse you so I also suggest reading copy elision (the first part about mandatory elision).
I'm not a C++ expert so take all of this with a grain of salt.

Accumulator factory in Haskell

Now, at the start of my adventure with programming I have some problems understanding basic concepts. Here is one related to Haskell or perhaps generally functional paradigm.
Here is a general statement of accumulator factory problem, from
http://rosettacode.org/wiki/Accumulator_factory
[Write a function that]
Takes a number n and returns a function (lets call it g), that takes a number i, and returns n incremented by the accumulation of i from every call of function g(i).
Works for any numeric type-- i.e. can take both ints and floats and returns functions that can take both ints and floats. (It is not enough simply to convert all input to floats. An accumulator that has only seen integers must return integers.) (i.e., if the language doesn't allow for numeric polymorphism, you have to use overloading or something like that)
Generates functions that return the sum of every number ever passed to them, not just the most recent. (This requires a piece of state to hold the accumulated value, which in turn means that pure functional languages can't be used for this task.)
Returns a real function, meaning something that you can use wherever you could use a function you had defined in the ordinary way in the text of your program. (Follow your language's conventions here.)
Doesn't store the accumulated value or the returned functions in a way that could cause them to be inadvertently modified by other code. (No global variables or other such things.)
with, as I understand, a key point being:
"[...] creating a function that [...]
Generates functions that return the sum of every number ever passed to them, not just the most recent. (This requires a piece of state to hold the accumulated value, which in turn means that pure functional languages can't be used for this task.)"
We can find a Haskell solution on the same website and it seems to do just what the quote above says.
Here
http://rosettacode.org/wiki/Category:Haskell
it is said that Haskell is purely functional.
What is then the explanation of the apparent contradiction? Or maybe there is no contradiction and I simply lack some understanding? Thanks.
The Haskell solution does not actually quite follow the rules of the challenge. In particular, it violates the rule that the function "Returns a real function, meaning something that you can use wherever you could use a function you had defined in the ordinary way in the text of your program." Instead of returning a real function, it returns an ST computation that produces a function that itself produces more ST computations. Within the context of an ST "state thread", you can create and use mutable references (STRef), arrays, and vectors. However, it's impossible for this mutable state to "leak" outside the state thread to contaminate pure code.

How does Rust find out the upper limit of an iterator?

I'm going through a couple of Rust examples, and there's a particular snippet of code which I don't really understand how it works. In particular, this example of Higher Order Functions. My focus is on this snippet of code:
let sum_of_squared_odd_numbers: u32 =
(0..).map(|n| n * n) // All natural numbers squared
.take_while(|&n| n < upper) // Below upper limit
.filter(|n| is_odd(*n)) // That are odd
.fold(0, |sum, i| sum + i); // Sum them
Here are my questions:
How does the compiler know when (0..) ends? Is the loop unrolled at compile time and are all the lambdas evaluated?
Isn't this extremely memory inefficient compared to the imperative version? For example (0..).map(|n| n * n) alone would end up taking O(n) memory.
How does the compiler know when (0..) ends?
The compiler doesn't know at all. That is a range literal, specifically a RangeFrom. Note that it implements the Iterator trait. The core piece of Iterator is next:
fn next(&mut self) -> Option<Self::Item>
That is, given a mutable borrow to the iterator, it can return another item (Some) or signal that there are no more items (None). It is completely possible to have iterators that go on forever.
In this particular example:
The range will yield every unsigned 32-bit number before stopping †.
The map will stop when the underlying iterator stops.
The take_while will stop when the predicate fails or the underlying iterator stops.
The filter will stop when the underlying iterator stops.
The fold will stop when the underlying iterator stops.
Isn't this extremely memory inefficient compared to the imperative version?
Nope! In fact, the compiler is very likely to compile this to the same code as the imperative version! You'd want to check the LLVM IR or assembly to be 100% sure, but Rust's monomorphization capabilities combined with LLVM's optimizer do some pretty amazing things.
Each iterator adapter pulls just enough items from the previous adapter to calculate the next value. In your example, I'd expect a constant memory allocation for the entire process.
The only component of the pipeline that requires any extra space would be the fold, and it just needs an accumulator value that is an u32. All the other adapters have no extra state.
An important thing to note is that calling the map, filter, and take_while iterator adaptors doesn't do any iterator computation at that point in time. They simply return new objects:
// Note the type is
// Filter<TakeWhile<Map<RangeFrom<_>, [closure]>, [closure]>, [closure]>
let () =
(0..)
.map(|n| n * n)
.take_while(|&n| n < 20)
.filter(|n| n % 2 == 0);
// At this point, we still haven't even looked at a single value
Whenever you call next on the final adaptor, each layer of the adaptor stack does enough work to get the next value. In the original example, fold is an iterator terminator that consumes the entire iterator, calling next until there are no more values.
† As bluss points out, you don't really want to try and go past the maximum value of a range, as it will either panic or loop forever, depending on if it is built in debug or release mode.
The compiler doesn't know when (0..) ends. However the iterators are lazy (as is mentioned on the page you linked to) and the the .take_while(|&n| n < upper) statement will stop the sequence as soon as n is greater than or equal to upper
I am not a rust expert, but the comment
// All natural numbers squared
tells me, that the list (or stream, enumeration, whatever) cannot be fully evaluated. So I would put my bet on some kind of lazy evaluation.
In that case, the range would carry an internal state and upon every usage the next element is computed. Catamorphisms (fold, map) are then implemented by "storing" the respective implementation instead of evaluating it directly.
update: I overlooked the take_while part. This method seems to be responsible (also according to the comment), to actually force the evaluation. Everything before just computes the range abstract, i.e. without any concrete elements. This is possible because functions can be composed.
The standard example for such a behavior is haskell

How to minimize the garbage collection in Go?

Some times you could want to avoid/minimize the garbage collector, so I want to be sure about how to do it.
I think that the next one is correct:
Declare variables at the beginning of the function.
To use array instead of slice.
Any more?
To minimize garbage collection in Go, you must minimize heap allocations. To minimize heap allocations, you must understand when allocations happen.
The following things always cause allocations (at least in the gc compiler as of Go 1):
Using the new built-in function
Using the make built-in function (except in a few unlikely corner cases)
Composite literals when the value type is a slice, map, or a struct with the & operator
Putting a value larger than a machine word into an interface. (For example, strings, slices, and some structs are larger than a machine word.)
Converting between string, []byte, and []rune
As of Go 1.3, the compiler special cases this expression to not allocate: m[string(b)], where m is a map and b is a []byte
Converting a non-constant integer value to a string
defer statements
go statements
Function literals that capture local variables
The following things can cause allocations, depending on the details:
Taking the address of a variable. Note that addresses can be taken implicitly. For example a.b() might take the address of a if a isn't a pointer and the b method has a pointer receiver type.
Using the append built-in function
Calling a variadic function or method
Slicing an array
Adding an element to a map
The list is intended to be complete and I'm reasonably confident in it, but am happy to consider additions or corrections.
If you're uncertain of where your allocations are happening, you can always profile as others suggested or look at the assembly produced by the compiler.
Avoiding garbage is relatively straight forward. You need to understand where the allocations are being made and see if you can avoid the allocation.
First, declaring variables at the beginning of a function will NOT help. The compiler does not know the difference. However, human's will know the difference and it will annoy them.
Use of an array instead of a slice will work, but that is because arrays (unless dereferenced) are put on the stack. Arrays have other issues such as the fact that they are passed by value (copied) between functions. Anything on the stack is "not garbage" since it will be freed when the function returns. Any pointer or slice that may escape the function is put on the heap which the garbage collector must deal with at some point.
The best thing you can do is avoid allocation. When you are done with large bits of data which you don't need, reuse them. This is the method used in the profiling tutorial on the Go blog. I suggest reading it.
Another example besides the one in the profiling tutorial: Lets say you have an slice of type []int named xs. You continually append to the []int until you reach a condition and then you reset it so you can start over. If you do xs = nil, you are now declaring the underlying array of the slice as garbage to be collected. Append will then reallocate xs the next time you use it. If instead you do xs = xs[:0], you are still resetting it but keeping the old array.
For the most part, trying to avoid creating garbage is premature optimization. For most of your code it does not matter. But you may find every once in a while a function which is called a great many times that allocates a lot each time it is run. Or a loop where you reallocate instead of reusing. I would wait until you see the bottle neck before going overboard.

Resources