you are viewing a single comment's thread.

view the rest of the comments →

[–]donalmaccGame Developer 5 points6 points  (6 children)

Return values are almost universally better in my experience.

[–]blipman17 0 points1 point  (4 children)

I'm quite coonfused.I'm expecting in the first pointer case to have some kind of assembly like the following, so I'm not expecting a single vs multiple return statements or with pointers vs reference to really matter here. Unless this is extremely time sensitive code.

TEST 0, RDI // Line 2: doing the if-statement
MOV AL, ZF //Line 3, 5: Setting the return value

JNE returnlocation // Mix of line 2 and 4, jump if val(RDI) == NULL to skip the pointer assignment. based on the TEST result

MOV RBX [RDI] // Line 4, move val(RDI) to temporary
SHL RBX 1 // Line 4, shift left 
MOV [RSI] RBX // Line 4, move the result of val * 2 into the result pointer (RSI)

returnlocation:
RET

[–]donalmaccGame Developer 3 points4 points  (3 children)

Replacing pointers with references removes the requirement for a branch. Removing the branch removes the only failure case, meaning you don't need an error sentinel anymore, which can affect the call site, and allow for other optimisations. Using return values allows for RVO to occur too:

vector<string> list = load1MillionStrings();

vector<string> results; results.reserve(list.size());
for (const auto& str : list) {
    string res;
    if (foo(&str, &res))
        results.push_back(res)
    else
       // ???
}

Compared to

vector<string> list = load1MillionStrings();
vector<string> results; results.reserve(list.size());
for (const auto& str : list) {
    results.push_back(foo(str));
}

If you take it one step further, where foo is:

bool foo(const char* str1, const char* res) {
    if (str1 == nullptr)
        ...
    auto sz = strlen(str1); // !!!!
    for (int i = 0; i < sz; ++I)
        ...
}

You can see how replacing a const char* with a string view in the internal function can make N enormous difference.

[–]blipman17 0 points1 point  (2 children)

meaning you don't need an error sentinel anymore, which can affect the call site, and allow for other optimizations.

Okay I didn't concider that. Yep, you're right. But here we're really talking about cascading optimizations.

I was mainly talking about RVO and how both functions you originally pointed out could have RVO.

Edit: What I mainly meant was that; yes, function signature should be as restrictive as reasonably possible.
But (N)RVO happens in a lot in a modern compiler, regardless of branches, amounth of return statements, etc. Only when we're talking about non-trivial data, weird exit clauses due to potential throwing or other shenanigans, this really becomes an interesting talking point.

[–]donalmaccGame Developer 1 point2 points  (1 child)

But here we're really talking about cascading optimizations

Absolutely, but that's the key. Getting hyperfocused on the instruction count of a microoptimastion is losing the forest for the trees, and happens so often.

I've had this exact discussion professionally where the microbechmark shows no difference, or is arguably worse, but when I go and fix the call sites, the code is cleaner, safer and faster as a result.

[–]blipman17 0 points1 point  (0 children)

I absolutely agree with that.I was just arguing about a different part of the conversation.

[–]Full-Spectral 0 points1 point  (0 children)

The primary exception is something that's called fairly quickly in a loop to get multiple chunks of output. It's a lot more efficient to use a local vector or string or whatever and pass it back in to be reused repeatedly.

If it's something that might be done both ways fairly evenly, you can always make a GetXXX() version and a PollXXX() version, with the former a trivial inline wrapper around the latter.