Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

unorderedmap: adding missing constexpr #6752

Open
wants to merge 1 commit into
base: develop
Choose a base branch
from

Conversation

romintomasetti
Copy link
Contributor

This PR adds missing constexpr in Kokkos::UnorderedMap.

@romintomasetti romintomasetti force-pushed the unorderedmap-constexpr branch 3 times, most recently from f46c73b to c60ad81 Compare January 29, 2024 08:38
@masterleinad
Copy link
Contributor

Why does it matter here to use if constexpr?

@romintomasetti
Copy link
Contributor Author

Why does it matter here to use if constexpr?

@masterleinad What do you mean by here ?

@masterleinad
Copy link
Contributor

@masterleinad What do you mean by here ?

It doesn't look like replacing if with if constexpr doesn't make any difference for the places you change in this pull request, does it?

@romintomasetti
Copy link
Contributor Author

romintomasetti commented Jan 29, 2024

Good question!

I quickly drafted this code to check that with gcc-12:

#include <cstdio>
#include <cstdlib>
#include <type_traits>
#include <utility>

#ifndef CONSTEXPR_OR_NOT
    #error "CONSTEXPR_OR_NOT must be defined."
#endif

template <typename value_t>
class UnorderedMap
{
public:
    static constexpr bool is_set = !std::is_same_v<value_t, double>;

public:
    UnorderedMap(const size_t size)
    {
        array = new value_t[size];

        printf("This is a %s.\n", is_set ? "set" : "map");
    }

    ~UnorderedMap() { delete[] array; }

    template <typename T>
    void __attribute__((noinline)) insert(const size_t index, T&& value)
    {
        if CONSTEXPR_OR_NOT (!is_set)
        {
            printf("Inserting a new value at index %zu\n", index);
            array[index] = std::forward<value_t>(value);
        }
        else
        {
            printf("Not inserting a new value at index %zu\n", index);
        }
    }

    auto get(const size_t index) const { return array[index]; }

private:
    value_t* array;
};

int main()
{
    UnorderedMap<double> umap(5);
    UnorderedMap<int   > uset(5);

    umap.insert(0, double(4.2));
    uset.insert(0, int(42));

    if( ! umap.is_set) printf("Value is %.5f.\n", umap.get(0));
    if(   uset.is_set) printf("Value is %d.\n"  , uset.get(0));

    return EXIT_SUCCESS;
};

and compile with:

set -ex

g++-12 -Werror -std=c++17 -O0 -fverbose-asm -DCONSTEXPR_OR_NOT=constexpr test.cpp -S -o test_wi_constexpr.s
g++-12 -Werror -std=c++17 -O0 -fverbose-asm -DCONSTEXPR_OR_NOT=          test.cpp -S -o test_wo_constexpr.s

g++-12 -Werror -std=c++17 -O0 -fverbose-asm -DCONSTEXPR_OR_NOT=constexpr test.cpp -o test_wi_constexpr
g++-12 -Werror -std=c++17 -O0 -fverbose-asm -DCONSTEXPR_OR_NOT=          test.cpp -o test_wo_constexpr

./test_wi_constexpr
./test_wo_constexpr

Then, compare the assembly output.

With gcc-12 you're right that the compiler will always evaluate the if as if it was a constexpr if because it knows that is_set is constexpr, and so it makes this optimization for you under-the-hood.

However, I couldn't find anything in the standard that forces a compiler to do so. So my guess is that not all compilers would do that. Am I right ? (if so I suggest we go forward with this PR - though it's rather a style fix rather than a performance fix - at least for gcc-12)

Interesting read: https://www.learncpp.com/cpp-tutorial/constexpr-if-statements/.

EDIT: It seems that nvcc 12.2 does the same for device code (i.e. assume constexpr if when the statement is a constant expression).

@masterleinad
Copy link
Contributor

If there is no measurable benefit, I'm not in favor of this pull request to reduce churn.

@romintomasetti
Copy link
Contributor Author

@masterleinad I understand your point of view. It's true that there is no performance gain.

However, in light of the (IMO very good) SO answer https://stackoverflow.com/a/54545744, I still think it's best practice to write constexpr in this case.

@romintomasetti romintomasetti force-pushed the unorderedmap-constexpr branch 2 times, most recently from b2b3a6c to fc08dfb Compare January 30, 2024 11:39
@romintomasetti
Copy link
Contributor Author

For the record - this PR extracts changes from #6584.

@dalg24
Copy link
Member

dalg24 commented Jan 30, 2024

@masterleinad I understand your point of view. It's true that there is no performance gain.

However, in light of the (IMO very good) SO answer https://stackoverflow.com/a/54545744, I still think it's best practice to write constexpr in this case.

I would agree if we were writing new code. But these changes are mostly unnecessary and you will find plenty of other guidelines that tell you "if it ain't broke, don't fix it"

uint32_t list = m_map.m_hash_lists(i);
for (size_type curr = list, ii = 0; curr != invalid_index;
for (size_type curr = list, ii = 0; curr != map_type::invalid_index;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avoid unrelated changes

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought it would be a related change... I reverted and simply added constexpr in the declaration of invalid_index.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR description is what should defend what is in scope and what is not.
It says "add[ing] missing constexpr in UnorderedMap" and early versions of this PR mixed in more changes which does not help building a good rational for changing code.

The constexpr is "missing" as in we could/should add it? Is it clearly not a bug but how does the change improve things?
You need to pick your battles. If what is most important to you is that maps with const value type should not be insertable, then focus on that.

@@ -526,7 +526,8 @@ class UnorderedMap {
/// Kokkos::UnorderedMapInsertOpTypes for more ops.
template <typename InsertOpType = default_op_type>
KOKKOS_INLINE_FUNCTION insert_result
insert(key_type const &k, impl_value_type const &v = impl_value_type(),
insert(key_type const &k,
[[maybe_unused]] impl_value_type const &v = impl_value_type(),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The [[maybe_unused]] attribute on arguments of a function of a function whose implementation does not fit on one page is an example of "code smell", that is to say I am not convinced since is an improvement whatsoever.

Copy link
Contributor Author

@romintomasetti romintomasetti Jan 30, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was "forced" by the compiler to add [[maybe_unused]] because this PR guards some parts of the insert code with if constexpr (instead of if). Therefore, compiling with a void value type (i.e. making a set instead of a map) makes these code blocks guarded by if constexpr not instantiated, and so the compiler complains:

uho you have v argument that is unused

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(So yes, the addition of constexpr is not just a useless change, it actually changes how compilers behave - for good in this case IMO since it's now clearly written that v might be unused, and that's the case for a set)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand what made you add the attribute. I am just saying that it is not clear to me that the code is better (as in more readable, more maintainable) now than before.

@nliber
Copy link
Contributor

nliber commented Feb 14, 2024

If we are going to do anything at all, it seems to me that the fundamental problem is that when the container is a set, the signature of insert should be

insert(key_type const&);

I know it is technically a breaking change, but can we just do that? That seems like a positive change for the public facing API. That being said, I don't know if that is even worth it, as it seems very unlikely that folks are calling this on a set with more than one specified parameter.

But that doesn't really address the disagreement here. The code with [[maybe_unused]] looks worse to me, because as a reader I have to reason out the circumstances that it is unused (and it doesn't help that is_set is a terrible name because as a reader I have to know that it refers to the type of the container and not referring to something like the state of the container being valid). And this seems like a lot of churn for a theoretical micro-optimization (one branch in a very complicated function that in practice gets optimized away). If that is really the concern, should we also be concerned about creating unused parameters (which also get optimized away in practice)?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants