Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
181 changes: 139 additions & 42 deletions runtime/cpp/emboss_memory_util.h
Original file line number Diff line number Diff line change
Expand Up @@ -47,31 +47,29 @@ struct MemoryAccessor {
using ChainedAccessor =
MemoryAccessor<CharT, kAlignment / 2, kOffset % (kAlignment / 2), kBits>;
using Unsigned = typename LeastWidthInteger<kBits>::Unsigned;
static inline Unsigned ReadLittleEndianUInt(const CharT *bytes) {

static inline Unsigned ReadLittleEndianUInt(const CharT* bytes) {
return ChainedAccessor::ReadLittleEndianUInt(bytes);
}
static inline void WriteLittleEndianUInt(CharT *bytes, Unsigned value) {

static inline void WriteLittleEndianUInt(CharT* bytes, Unsigned value) {
ChainedAccessor::WriteLittleEndianUInt(bytes, value);
}
static inline Unsigned ReadBigEndianUInt(const CharT *bytes) {

static inline Unsigned ReadBigEndianUInt(const CharT* bytes) {
return ChainedAccessor::ReadBigEndianUInt(bytes);
}
static inline void WriteBigEndianUInt(CharT *bytes, Unsigned value) {

static inline void WriteBigEndianUInt(CharT* bytes, Unsigned value) {
ChainedAccessor::WriteBigEndianUInt(bytes, value);
}
};

// The least-aligned case for MemoryAccessor is 8-bit alignment, and the default
// version of MemoryAccessor will devolve to this one if there is no more
// specific override.
//
// If the system byte order is known, then these routines can use memcpy and
// (possibly) a byte swap; otherwise they can read individual bytes and
// shift+or them together in the appropriate order. I (bolms@) haven't found a
// compiler that will optimize the multiple reads, shifts, and ors into a single
// read, so the memcpy version is *much* faster for 32-bit and larger reads.
// UnalignedAccessor provides generic, iterator-compatible read and write
// implementations for unaligned data. It uses std::copy_n to safely handle
// data movement and byte reordering across contiguous or fragmented storage.
template <typename CharT, ::std::size_t kBits>
struct MemoryAccessor<CharT, 1, 0, kBits> {
struct UnalignedAccessor {
static_assert(kBits % 8 == 0,
"MemoryAccessor can only read and write whole-byte values.");
static_assert(IsAliasSafe<CharT>::value,
Expand All @@ -80,32 +78,65 @@ struct MemoryAccessor<CharT, 1, 0, kBits> {
using Unsigned = typename LeastWidthInteger<kBits>::Unsigned;

#if defined(EMBOSS_LITTLE_ENDIAN_TO_NATIVE)
static inline Unsigned ReadLittleEndianUInt(const CharT *bytes) {
template <typename Iterator>
static inline Unsigned ReadLittleEndianUInt(Iterator bytes) {
using DereferenceT = typename ::std::remove_cv<
typename ::std::remove_reference<decltype(*bytes)>::type>::type;
static_assert(
::std::is_same<DereferenceT,
typename ::std::remove_cv<CharT>::type>::value,
"UnalignedAccessor Iterator underlying type must match CharT.");
Unsigned result = 0;
::std::memcpy(&result, bytes, kBits / 8);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The overall concern here would be that we're removing the use of memcpy in favor of copy_n in a few places. This might be a regression on some systems, I wonder if it's worth doing some template shenanigans to still use memcpy if we have a pointer type for the iterator. See bolms@'s note above about this code being perf sensitive.

auto dest =
reinterpret_cast<typename ::std::remove_cv<CharT>::type*>(&result);
::std::copy_n(bytes, kBits / 8, dest);
return EMBOSS_LITTLE_ENDIAN_TO_NATIVE(result);
}
#else
static inline Unsigned ReadLittleEndianUInt(const CharT *bytes) {
template <typename Iterator>
static inline Unsigned ReadLittleEndianUInt(Iterator bytes) {
using DereferenceT = typename ::std::remove_cv<
typename ::std::remove_reference<decltype(*bytes)>::type>::type;
static_assert(
::std::is_same<DereferenceT,
typename ::std::remove_cv<CharT>::type>::value,
"UnalignedAccessor Iterator underlying type must match CharT.");
Unsigned result = 0;
for (decltype(kBits) i = 0; i < kBits / 8; ++i) {
result |=
static_cast<Unsigned>(static_cast</**/ ::std::uint8_t>(bytes[i]))
<< i * 8;
auto it = bytes;
for (decltype(kBits) i = 0; i < kBits / 8; ++i, ++it) {
result |= static_cast<Unsigned>(static_cast</**/ ::std::uint8_t>(*it))
<< i * 8;
}
return result;
}
#endif

#if defined(EMBOSS_NATIVE_TO_LITTLE_ENDIAN)
static inline void WriteLittleEndianUInt(CharT *bytes, Unsigned value) {
template <typename Iterator>
static inline void WriteLittleEndianUInt(Iterator bytes, Unsigned value) {
using DereferenceT = typename ::std::remove_cv<
typename ::std::remove_reference<decltype(*bytes)>::type>::type;
static_assert(
::std::is_same<DereferenceT,
typename ::std::remove_cv<CharT>::type>::value,
"UnalignedAccessor Iterator underlying type must match CharT.");
value = EMBOSS_NATIVE_TO_LITTLE_ENDIAN(value);
::std::memcpy(bytes, &value, kBits / 8);
auto src =
reinterpret_cast<const typename ::std::remove_cv<CharT>::type*>(&value);
::std::copy_n(src, kBits / 8, bytes);
}
#else
static inline void WriteLittleEndianUInt(CharT *bytes, Unsigned value) {
for (decltype(kBits) i = 0; i < kBits / 8; ++i) {
bytes[i] = static_cast<CharT>(static_cast</**/ ::std::uint8_t>(value));
template <typename Iterator>
static inline void WriteLittleEndianUInt(Iterator bytes, Unsigned value) {
using DereferenceT = typename ::std::remove_cv<
typename ::std::remove_reference<decltype(*bytes)>::type>::type;
static_assert(
::std::is_same<DereferenceT,
typename ::std::remove_cv<CharT>::type>::value,
"UnalignedAccessor Iterator underlying type must match CharT.");
auto it = bytes;
for (decltype(kBits) i = 0; i < kBits / 8; ++i, ++it) {
*it = static_cast<DereferenceT>(static_cast</**/ ::std::uint8_t>(value));
if (sizeof value > 1) {
// Shifting an 8-bit type by 8 bits is undefined behavior, so skip this
// step for uint8_t.
Expand All @@ -116,7 +147,14 @@ struct MemoryAccessor<CharT, 1, 0, kBits> {
#endif

#if defined(EMBOSS_BIG_ENDIAN_TO_NATIVE)
static inline Unsigned ReadBigEndianUInt(const CharT *bytes) {
template <typename Iterator>
static inline Unsigned ReadBigEndianUInt(Iterator bytes) {
using DereferenceT = typename ::std::remove_cv<
typename ::std::remove_reference<decltype(*bytes)>::type>::type;
static_assert(
::std::is_same<DereferenceT,
typename ::std::remove_cv<CharT>::type>::value,
"UnalignedAccessor Iterator underlying type must match CharT.");
Unsigned result = 0;
// When a big-endian source integer is smaller than the result, the source
// bytes must be copied into the final bytes of the destination. This is
Expand Down Expand Up @@ -160,35 +198,59 @@ struct MemoryAccessor<CharT, 1, 0, kBits> {
// +--------+--------+--------+--------+
// | 0x00 | 0x11 | 0x22 | 0x33 |
// +--------+--------+--------+--------+
::std::memcpy(reinterpret_cast<char *>(&result) + sizeof result - kBits / 8,
bytes, kBits / 8);
auto dest =
reinterpret_cast<typename ::std::remove_cv<CharT>::type*>(&result) +
sizeof(result) - kBits / 8;
::std::copy_n(bytes, kBits / 8, dest);
result = EMBOSS_BIG_ENDIAN_TO_NATIVE(result);
return result;
}
#else
static inline Unsigned ReadBigEndianUInt(const CharT *bytes) {
template <typename Iterator>
static inline Unsigned ReadBigEndianUInt(Iterator bytes) {
using DereferenceT = typename ::std::remove_cv<
typename ::std::remove_reference<decltype(*bytes)>::type>::type;
static_assert(
::std::is_same<DereferenceT,
typename ::std::remove_cv<CharT>::type>::value,
"UnalignedAccessor Iterator underlying type must match CharT.");
Unsigned result = 0;
for (decltype(kBits) i = 0; i < kBits / 8; ++i) {
result |=
static_cast<Unsigned>(static_cast</**/ ::std::uint8_t>(bytes[i]))
<< (kBits - 8 - i * 8);
auto it = bytes;
for (decltype(kBits) i = 0; i < kBits / 8; ++i, ++it) {
result |= static_cast<Unsigned>(static_cast</**/ ::std::uint8_t>(*it))
<< (kBits - 8 - i * 8);
}
return result;
}
#endif

#if defined(EMBOSS_NATIVE_TO_BIG_ENDIAN)
static inline void WriteBigEndianUInt(CharT *bytes, Unsigned value) {
template <typename Iterator>
static inline void WriteBigEndianUInt(Iterator bytes, Unsigned value) {
using DereferenceT = typename ::std::remove_cv<
typename ::std::remove_reference<decltype(*bytes)>::type>::type;
static_assert(
::std::is_same<DereferenceT,
typename ::std::remove_cv<CharT>::type>::value,
"UnalignedAccessor Iterator underlying type must match CharT.");
value = EMBOSS_NATIVE_TO_BIG_ENDIAN(value);
::std::memcpy(bytes,
reinterpret_cast<char *>(&value) + sizeof value - kBits / 8,
kBits / 8);
auto src = reinterpret_cast<const typename ::std::remove_cv<CharT>::type*>(
&value) +
sizeof(value) - kBits / 8;
::std::copy_n(src, kBits / 8, bytes);
}
#else
static inline void WriteBigEndianUInt(CharT *bytes, Unsigned value) {
for (decltype(kBits) i = 0; i < kBits / 8; ++i) {
bytes[kBits / 8 - 1 - i] =
static_cast<CharT>(static_cast</**/ ::std::uint8_t>(value));
template <typename Iterator>
static inline void WriteBigEndianUInt(Iterator bytes, Unsigned value) {
using DereferenceT = typename ::std::remove_cv<
typename ::std::remove_reference<decltype(*bytes)>::type>::type;
static_assert(
::std::is_same<DereferenceT,
typename ::std::remove_cv<CharT>::type>::value,
"UnalignedAccessor Iterator underlying type must match CharT.");
auto it = bytes + (kBits / 8 - 1);
for (decltype(kBits) i = 0; i < kBits / 8; ++i, --it) {
*it = static_cast<DereferenceT>(static_cast</**/ ::std::uint8_t>(value));
if (sizeof value > 1) {
// Shifting an 8-bit type by 8 bits is undefined behavior, so skip this
// step for uint8_t.
Expand All @@ -199,6 +261,41 @@ struct MemoryAccessor<CharT, 1, 0, kBits> {
#endif
};

// The least-aligned case for MemoryAccessor is 8-bit alignment, and the default
// version of MemoryAccessor will devolve to this one if there is no more
// specific override.
//
// If the system byte order is known, then these routines can use memcpy and
// (possibly) a byte swap; otherwise they can read individual bytes and
// shift+or them together in the appropriate order. I (bolms@) haven't found a
// compiler that will optimize the multiple reads, shifts, and ors into a single
// read, so the memcpy version is *much* faster for 32-bit and larger reads.
template <typename CharT, ::std::size_t kBits>
struct MemoryAccessor<CharT, 1, 0, kBits> {
static_assert(kBits % 8 == 0,
"MemoryAccessor can only read and write whole-byte values.");
static_assert(IsAliasSafe<CharT>::value,
"MemoryAccessor can only be used on pointers to char types.");

using Unsigned = typename LeastWidthInteger<kBits>::Unsigned;

static inline Unsigned ReadLittleEndianUInt(const CharT* bytes) {
return UnalignedAccessor<CharT, kBits>::ReadLittleEndianUInt(bytes);
}

static inline void WriteLittleEndianUInt(CharT* bytes, Unsigned value) {
UnalignedAccessor<CharT, kBits>::WriteLittleEndianUInt(bytes, value);
}

static inline Unsigned ReadBigEndianUInt(const CharT* bytes) {
return UnalignedAccessor<CharT, kBits>::ReadBigEndianUInt(bytes);
}

static inline void WriteBigEndianUInt(CharT* bytes, Unsigned value) {
UnalignedAccessor<CharT, kBits>::WriteBigEndianUInt(bytes, value);
}
};

// Specialization of UIntMemoryAccessor for 16- 32- and 64-bit-aligned reads and
// writes, using EMBOSS_ALIAS_SAFE_POINTER_CAST instead of memcpy.
#if defined(EMBOSS_ALIAS_SAFE_POINTER_CAST) && \
Expand Down
1 change: 1 addition & 0 deletions runtime/cpp/test/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ emboss_cc_util_test(
copts = ["-DEMBOSS_FORCE_ALL_CHECKS"],
deps = [
"//runtime/cpp:cpp_utils",
"//runtime/cpp/test/util:noncontiguous_container",
"@com_google_googletest//:gtest_main",
],
)
Expand Down
98 changes: 98 additions & 0 deletions runtime/cpp/test/emboss_memory_util_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
#include "gtest/gtest.h"
#include "runtime/cpp/emboss_memory_util.h"
#include "runtime/cpp/emboss_prelude.h"
#include "runtime/cpp/test/util/noncontiguous_container.h"

namespace emboss {
namespace support {
Expand Down Expand Up @@ -150,6 +151,103 @@ TEST(MemoryAccessor, LittleEndianReads) {
TestMemoryAccessor<unsigned char, 8, 0, 64>();
}

template <typename CharT, ::std::size_t kBits>
void TestIteratorUnalignedAccessor() {
using ByteT = typename std::remove_const<CharT>::type;

// Test all 2^7 = 128 possible ways to partition an 8 byte buffer into chunks.
// A 1 bit in `partition_mask` at bit `i` means a chunk boundary exists
// between byte `i` and `i + 1`.
for (uint8_t partition_mask = 0; partition_mask < 128; ++partition_mask) {
std::vector<std::vector<ByteT>> chunks;
std::vector<ByteT> current_chunk;

for (uint8_t i = 0; i < 8; ++i) {
current_chunk.push_back(ByteT(i + 1));
if ((partition_mask & (1 << i)) || i == 7) {
chunks.push_back(std::move(current_chunk));
current_chunk.clear();
}
}

NoncontiguousContainer<ByteT> container(chunks);
EXPECT_EQ(container.size(), 8U);

EXPECT_EQ(0x0807060504030201UL & (~0x0UL >> (64 - kBits)),
(UnalignedAccessor<CharT, kBits>::ReadLittleEndianUInt(
container.begin())))
<< "Read LE Failed. Mask = " << static_cast<int>(partition_mask)
<< "; kBits = " << kBits;

EXPECT_EQ(
0x0102030405060708UL >> (64 - kBits),
(UnalignedAccessor<CharT, kBits>::ReadBigEndianUInt(container.begin())))
<< "Read BE Failed. Mask = " << static_cast<int>(partition_mask)
<< "; kBits = " << kBits;

auto write_container_le = container;
UnalignedAccessor<CharT, kBits>::WriteLittleEndianUInt(
write_container_le.begin(),
0x7172737475767778UL & (~0x0UL >> (64 - kBits)));

auto expected_vector_after_write_le = init_container<std::vector<ByteT>>(
0x78, 0x77, 0x76, 0x75, 0x74, 0x73, 0x72, 0x71);
for (typename std::vector<ByteT>::size_type i = kBits / 8; i < 8; ++i) {
expected_vector_after_write_le[i] = ByteT(i + 1);
}

std::vector<ByteT> actual_vector_le;
for (auto it = write_container_le.begin(); it != write_container_le.end();
++it) {
actual_vector_le.push_back(*it);
}
EXPECT_EQ(expected_vector_after_write_le, actual_vector_le)
<< "Write LE Failed. Mask = " << static_cast<int>(partition_mask)
<< "; kBits = " << kBits;

auto write_container_be = container;
UnalignedAccessor<CharT, kBits>::WriteBigEndianUInt(
write_container_be.begin(), 0x7172737475767778UL >> (64 - kBits));

auto expected_vector_after_write_be = init_container<std::vector<ByteT>>(
0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78);
for (typename std::vector<ByteT>::size_type i = kBits / 8; i < 8; ++i) {
expected_vector_after_write_be[i] = ByteT(i + 1);
}

std::vector<ByteT> actual_vector_be;
for (auto it = write_container_be.begin(); it != write_container_be.end();
++it) {
actual_vector_be.push_back(*it);
}
EXPECT_EQ(expected_vector_after_write_be, actual_vector_be)
<< "Write BE Failed. Mask = " << static_cast<int>(partition_mask)
<< "; kBits = " << kBits;
}

// Recursively iterate the template:
TestIteratorUnalignedAccessor<CharT, kBits - 8>();
}

template <>
void TestIteratorUnalignedAccessor<char, 0>() {}

#if __cplusplus >= 201703L
template <>
void TestIteratorUnalignedAccessor<std::byte, 0>() {}
#endif

template <>
void TestIteratorUnalignedAccessor<unsigned char, 0>() {}

TEST(UnalignedAccessor, IteratorReadsAndWrites) {
TestIteratorUnalignedAccessor<char, 64>();
#if __cplusplus >= 201703L
TestIteratorUnalignedAccessor<std::byte, 64>();
#endif
TestIteratorUnalignedAccessor<unsigned char, 64>();
}

TEST(ContiguousBuffer, OffsetStorageType) {
EXPECT_TRUE((::std::is_same<
ContiguousBuffer<char, 2, 0>,
Expand Down
18 changes: 18 additions & 0 deletions runtime/cpp/test/util/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
load("@rules_cc//cc:cc_library.bzl", "cc_library")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

nit: copyright header?

load("//runtime/cpp/test:build_defs.bzl", "emboss_cc_util_test")

cc_library(
name = "noncontiguous_container",
hdrs = ["noncontiguous_container.h"],
visibility = ["//runtime/cpp/test:__pkg__", "//visibility:public"],
)

emboss_cc_util_test(
name = "noncontiguous_container_test",
srcs = ["noncontiguous_container_test.cc"],
deps = [
":noncontiguous_container",
"//runtime/cpp:cpp_utils",
"@com_google_googletest//:gtest_main",
],
)
Loading