Skip to content

Implement lane-wise modulo. #86

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

Open
wants to merge 44 commits into
base: master
Choose a base branch
from
Open

Implement lane-wise modulo. #86

wants to merge 44 commits into from

Conversation

jamierpond
Copy link
Collaborator

@jamierpond jamierpond commented May 26, 2024

No safety about the subtract one, but shouldn't be an issue if your values are in range.

Depends on the "ergonomics" PR, which should merge first, and then the "base" of this PR should be moved to master.

@jamierpond jamierpond changed the base branch from master to jp/swar-to-array May 26, 2024 07:54
thecppzoo added 2 commits May 26, 2024 13:26
Reverting mistake, this is not a `struct` definition but a variable!
#include <cstddef>
#include <cstdint>

namespace zoo::math {
Copy link
Owner

Choose a reason for hiding this comment

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

You know, should you have used maths, I would have let it slide. But since you didn't, if you try to use the British maths I will reject the renaming on the basis that there is no need to make a change like that
;-)

Copy link
Collaborator

Choose a reason for hiding this comment

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

I wouldn't have let 'maths' slide.

template <typename IntegerType = size_t>
constexpr static
std::enable_if_t<std::is_integral_v<IntegerType>, bool>
is_power_of_two(IntegerType x) noexcept {
Copy link
Owner

Choose a reason for hiding this comment

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

Why snake_case instead of the normal camelCase and DromedaryCamelCase customary in the rest of zoo for these things?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

habit, let's move to camelCase to be consistent. will start email thread about case to ask if you have strong preference to avoid polluting PR with unrelated discussion.

#pragma once
#include "SWAR.h"
#include <cstddef>
#include <cstdint>
Copy link
Owner

Choose a reason for hiding this comment

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

SWAR.h includes this, or its equivalent, stdint.h, I see no point in things like std::uint64_t instead of just uint64_t

constexpr static
std::enable_if_t<std::is_integral_v<IntegerType>, bool>
is_power_of_two(IntegerType x) noexcept {
return x > 0 && (x & (x - 1)) == 0;
Copy link
Owner

Choose a reason for hiding this comment

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

Indentation.
Why the X > 0 instead of the customary X or bool(X)?

Comment on lines 104 to 114
static_assert(equals(S{S::Literal, {1, 2, 3, 4}},
S{S::Literal, {1, 2, 3, 4}}).value()
== BS{BS::Literal, {T, T, T, T}}.value());

static_assert(equals(S{S::Literal, {1, 2, 3, 4}},
S{S::Literal, {5, 6, 7, 8}}).value()
== BS{BS::Literal, {F, F, F, F}}.value());

static_assert(equals(S{S::Literal, {1, 2, 3, 4}},
S{S::Literal, {5, 2, 7, 4}}).value()
== BS{BS::Literal, {F, T, F, T}}.value());
Copy link
Owner

Choose a reason for hiding this comment

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

NO ACCIDENTAL INDENTATION LEVELS

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

this is not accidental, but just not intentionally formatted yet. didn't feel super obvious to me what this should look like.

@thecppzoo
Copy link
Owner

I have repeatedly expressed that accidental indentation levels are contrary to the principles of how the sources ought to express more detail.
Besides, they have the practical effect of shifting the important part of the source code to the right, and an incentive to damage the code with wider lines.

@@ -63,6 +66,7 @@ constexpr std::make_unsigned_t<T> lsbIndex(T v) noexcept {
template<int NBits_, typename T = uint64_t>
struct SWAR {
using type = std::make_unsigned_t<T>;
using Boolean = BooleanSWAR<NBits_, T>;
Copy link
Owner

Choose a reason for hiding this comment

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

I don't like this forward reference.
It is not that intuition tells me that MSVC won't like this and give us subtle compilation problems because of MSVCs bugs, but something deeper. I don't think that the abstraction of BooleanSWAR is in any way something that SWAR ought to advertise. There's something fishy here that I can not express well.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yeah I wasn't sure about this either... wanted to see what you thought about this.

Comment on lines 31 to 33
static_assert(moduloPowerOfTwo<4>(0) == 0);
static_assert(moduloPowerOfTwo<8>(9) == 1);
static_assert(moduloPowerOfTwo<4096>(4097) == 1);
Copy link
Owner

@thecppzoo thecppzoo May 27, 2024

Choose a reason for hiding this comment

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

I know we have some pollution like this in our headers. It is pollution we haven't bothered to clean. Please move these legitimate compile time tests to their "translation unit" out of the header.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Good callout, thanks.

constexpr static auto isPowerOfTwo(S x) noexcept {
constexpr auto NBits = S::NBits;
using T = typename S::type;
auto greater_than_0 = greaterEqual_MSB_off(x, S{0});
Copy link
Owner

Choose a reason for hiding this comment

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

Does this work if the MSB is not off?
I think that, for example, a SWAR of 4 bits per lane with one lane containing the MSB, like for example 8, will fail.
I'm sorry that the _MSB_off is confusing

Comment on lines +59 to +66
std::enable_if_t<zoo::math::isPowerOfTwo<size_t, N>(), S>
moduloPowerOfTwo(const S x) noexcept {
constexpr auto N_minus_1 = N - 1;
constexpr auto N_in_lanes = zoo::meta::BitmaskMaker<typename S::type, N_minus_1, S::NBits>::value;
auto y = x.value() & N_in_lanes;
return S{y};
}

Copy link
Owner

Choose a reason for hiding this comment

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

The way this is formulated is perhaps not useful:
For integral types, when the compiler can prove the modulus is a power of two, the compiler will, for sure, use bitwise conjunction rather than the assembler for the modulo instruction.
There remains the question of when the compiler doesn't know whether the modulus is a power of two, this would be only at runtime, and then, programmers have explicitly accounted for this improvement since ages, I don't think this way of going about it will give them an incentive to use this primitive.
This is useful for "weird" types that represent integers, like multi-precision integers, I don't know, that do not satisfy the "Integral" property required by isPowerOfTwo.
There is one aspect that I really like aobut this code: the structural guarantee that you won't pass a non-power-of-two as template argument, I see myself making some code with lane sizes not powers of two, and in some remote part of the code there is an assumption broken of the lane size being a powr of two, invoking bitwise conjuction, for "hilarity".

@@ -1,11 +1,14 @@
#include "zoo/swar/SWAR.h"
Copy link
Owner

Choose a reason for hiding this comment

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

Why?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

probably an auto-import

Comment on lines +105 to +106
const bool (&expected)[S::Lanes]) {
return equals(S{S::Literal, left}, S{S::Literal, right}).value()
Copy link
Owner

Choose a reason for hiding this comment

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

Accidental levels of indentation, scopes are not closed at the indentation level.
Do you think moving the end of the argument list to the right helps the code in any way?
Why is the declaration of the three arguments more nested than the implementation?

S{S::Literal, {5, 2, 7, 4}}).value()
== BS{BS::Literal, {F, T, F, T}}.value());
template <typename S = S, typename BS = BS>
constexpr auto laneWiseEqualsTest(
Copy link
Owner

Choose a reason for hiding this comment

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

This is not right, if you want "all lanes equal" you use the underlying value comparison as in all other tests.
Unit tests ought to demonstrate how to use a codebase. Using per-lane comparisons for a whole-base-type comparison is inadequate.
There is a probem with equals which is that is terseness invites incorrect use.
Even if you use equals, the comparison against a BooleanSWAR of all true is inelegant:
not(boolean(~equals(, since we have the conversion to boolean

}
static_assert(laneWiseEqualsTest({1, 2, 3, 4}, {1, 2, 3, 4}, {1, 1, 1, 1}));
static_assert(laneWiseEqualsTest({1, 2, 3, 4}, {5, 6, 7, 8}, {0, 0, 0, 0}));
static_assert(laneWiseEqualsTest({1, 2, 3, 4}, {5, 2, 7, 4}, {0, 1, 0, 1}));
Copy link
Owner

Choose a reason for hiding this comment

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

This is legitimate as written

Comment on lines +109 to +110
static_assert(laneWiseEqualsTest({1, 2, 3, 4}, {1, 2, 3, 4}, {1, 1, 1, 1}));
static_assert(laneWiseEqualsTest({1, 2, 3, 4}, {5, 6, 7, 8}, {0, 0, 0, 0}));
Copy link
Owner

Choose a reason for hiding this comment

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

These are whole-BooleanSWAR comparisons that can be achieved via the boolean conversion

S{S::Literal, {5, 2, 7, 4}}).value()
== BS{BS::Literal, {F, T, F, T}}.value());
template <typename S = S, typename BS = BS>
constexpr auto laneWiseEqualsTest(
Copy link
Owner

Choose a reason for hiding this comment

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

laneWiseEquals is problematic:
What does it mean? there are results that are expected to be equal or different according to a collection of expected boolean values, that is not even suggested by the name.
Then, laneWiseEquals suggests LANE WISE EQUALS. I guess that you call your "expected" comparison "WISE EQUAL".
Notwithstanding other problems, perhaps lanewiseEqualityComparisonExpected?

}
static_assert(powerOfTwoTest<S, BS>({1, 2, 3, 4}, {1, 1, 0, 1}));
static_assert(powerOfTwoTest<S, BS>({2, 3, 64, 77}, {1, 0, 1, 0}));
static_assert(powerOfTwoTest<S, BS>({3, 65, 128, 0}, {0, 0, 1, 1}));
Copy link
Owner

Choose a reason for hiding this comment

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

This much copy&pasting is not valuable.
Did you consider packing all of the redundancy into an X macro?

Copy link
Owner

@thecppzoo thecppzoo left a comment

Choose a reason for hiding this comment

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

We need to document much better what are the primitives available and especially common idioms around them.
In this PR there is clear misuse of existing operations and the introduction of idioms that are not well justified, because the common idioms are not well known.
I've not worked in cleaning up the SWAR tests because I believe that they ought to be generated by a code generator where you express the intent and the test cases are generated and their source code.
We just have to stop making this more confusing by accidental misuse.

@thecppzoo
Copy link
Owner

thecppzoo commented May 27, 2024

The implementation look good.
I have a problem with the unit tests. Not even with what is tested, that seems right, but how things are tested, however semantically correct, they are misuse of the available operations.
A user attempting to use this operations with elegance and top performance would be more confused by these tests than without these tests. That users get confused with the codebase as it is, is powerful reason to not confuse more, not to accept more confusing tests because there are precedents of confusing tests.

@jamierpond jamierpond changed the base branch from jp/swar-to-array to master June 27, 2024 18:58
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.

3 participants