609 lines · c
1//===-- Generic implementation of memory function building blocks ---------===//2//3// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.4// See https://llvm.org/LICENSE.txt for license information.5// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception6//7//===----------------------------------------------------------------------===//8//9// This file provides generic C++ building blocks.10// Depending on the requested size, the block operation uses unsigned integral11// types, vector types or an array of the type with the maximum size.12//13// The maximum size is passed as a template argument. For instance, on x8614// platforms that only supports integral types the maximum size would be 815// (corresponding to uint64_t). On this platform if we request the size 32, this16// would be treated as a cpp::array<uint64_t, 4>.17//18// On the other hand, if the platform is x86 with support for AVX the maximum19// size is 32 and the operation can be handled with a single native operation.20//21//===----------------------------------------------------------------------===//22 23#ifndef LLVM_LIBC_SRC_STRING_MEMORY_UTILS_OP_GENERIC_H24#define LLVM_LIBC_SRC_STRING_MEMORY_UTILS_OP_GENERIC_H25 26#include "hdr/stdint_proxy.h"27#include "src/__support/CPP/array.h"28#include "src/__support/CPP/type_traits.h"29#include "src/__support/common.h"30#include "src/__support/endian_internal.h"31#include "src/__support/macros/attributes.h" // LIBC_INLINE32#include "src/__support/macros/config.h" // LIBC_NAMESPACE_DECL33#include "src/__support/macros/optimization.h"34#include "src/__support/macros/properties/compiler.h"35#include "src/__support/macros/properties/types.h" // LIBC_TYPES_HAS_INT6436#include "src/string/memory_utils/op_builtin.h"37#include "src/string/memory_utils/utils.h"38 39static_assert((UINTPTR_MAX == 4294967295U) ||40 (UINTPTR_MAX == 18446744073709551615UL),41 "We currently only support 32- or 64-bit platforms");42 43#ifdef LIBC_COMPILER_IS_MSVC44#ifdef LIBC_TARGET_ARCH_IS_X8645namespace LIBC_NAMESPACE_DECL {46using generic_v128 = __m128i;47using generic_v256 = __m256i;48using generic_v512 = __m512i;49} // namespace LIBC_NAMESPACE_DECL50#else51// Special handling when target does not have real vector types.52// We can potentially use uint8x16_t etc. However, MSVC does not provide53// subscript operation.54namespace LIBC_NAMESPACE_DECL {55struct alignas(16) generic_v128 : public cpp::array<uint8_t, 16> {};56struct alignas(32) generic_v256 : public cpp::array<uint8_t, 32> {};57struct alignas(64) generic_v512 : public cpp::array<uint8_t, 64> {};58} // namespace LIBC_NAMESPACE_DECL59#endif60 61#else62namespace LIBC_NAMESPACE_DECL {63// Compiler types using the vector attributes.64using generic_v128 = uint8_t __attribute__((__vector_size__(16)));65using generic_v256 = uint8_t __attribute__((__vector_size__(32)));66using generic_v512 = uint8_t __attribute__((__vector_size__(64)));67} // namespace LIBC_NAMESPACE_DECL68#endif // LIBC_COMPILER_IS_MSVC69 70namespace LIBC_NAMESPACE_DECL {71namespace generic {72 73// We accept three types of values as elements for generic operations:74// - scalar : unsigned integral types,75// - vector : compiler types using the vector attributes or platform builtins,76// - array : a cpp::array<T, N> where T is itself either a scalar or a vector.77// The following traits help discriminate between these cases.78 79template <typename T> struct is_scalar : cpp::false_type {};80template <> struct is_scalar<uint8_t> : cpp::true_type {};81template <> struct is_scalar<uint16_t> : cpp::true_type {};82template <> struct is_scalar<uint32_t> : cpp::true_type {};83#ifdef LIBC_TYPES_HAS_INT6484template <> struct is_scalar<uint64_t> : cpp::true_type {};85#endif // LIBC_TYPES_HAS_INT6486// Meant to match std::numeric_limits interface.87// NOLINTNEXTLINE(readability-identifier-naming)88template <typename T> constexpr bool is_scalar_v = is_scalar<T>::value;89 90template <typename T> struct is_vector : cpp::false_type {};91template <> struct is_vector<generic_v128> : cpp::true_type {};92template <> struct is_vector<generic_v256> : cpp::true_type {};93template <> struct is_vector<generic_v512> : cpp::true_type {};94// Meant to match std::numeric_limits interface.95// NOLINTNEXTLINE(readability-identifier-naming)96template <typename T> constexpr bool is_vector_v = is_vector<T>::value;97 98template <class T> struct is_array : cpp::false_type {};99template <class T, size_t N> struct is_array<cpp::array<T, N>> {100 // Meant to match std::numeric_limits interface.101 // NOLINTNEXTLINE(readability-identifier-naming)102 static constexpr bool value = is_scalar_v<T> || is_vector_v<T>;103};104// Meant to match std::numeric_limits interface.105// NOLINTNEXTLINE(readability-identifier-naming)106template <typename T> constexpr bool is_array_v = is_array<T>::value;107 108// Meant to match std::numeric_limits interface.109// NOLINTBEGIN(readability-identifier-naming)110template <typename T>111constexpr bool is_element_type_v =112 is_scalar_v<T> || is_vector_v<T> || is_array_v<T>;113// NOLINTEND(readability-identifier-naming)114 115// Helper struct to retrieve the number of elements of an array.116template <class T> struct array_size {};117template <class T, size_t N>118struct array_size<cpp::array<T, N>> : cpp::integral_constant<size_t, N> {};119// Meant to match std::numeric_limits interface.120// NOLINTNEXTLINE(readability-identifier-naming)121template <typename T> constexpr size_t array_size_v = array_size<T>::value;122 123// Generic operations for the above type categories.124 125template <typename T> T load(CPtr src) {126 static_assert(is_element_type_v<T>);127 if constexpr (is_scalar_v<T> || is_vector_v<T>) {128 return ::LIBC_NAMESPACE::load<T>(src);129 } else if constexpr (is_array_v<T>) {130 using value_type = typename T::value_type;131 T value;132 for (size_t i = 0; i < array_size_v<T>; ++i)133 value[i] = load<value_type>(src + (i * sizeof(value_type)));134 return value;135 }136}137 138template <typename T> void store(Ptr dst, T value) {139 static_assert(is_element_type_v<T>);140 if constexpr (is_scalar_v<T> || is_vector_v<T>) {141 ::LIBC_NAMESPACE::store<T>(dst, value);142 } else if constexpr (is_array_v<T>) {143 using value_type = typename T::value_type;144 for (size_t i = 0; i < array_size_v<T>; ++i)145 store<value_type>(dst + (i * sizeof(value_type)), value[i]);146 }147}148 149template <typename T> T splat(uint8_t value) {150 static_assert(is_scalar_v<T> || is_vector_v<T>);151 if constexpr (is_scalar_v<T>)152 return T(~0) / T(0xFF) * T(value);153 else if constexpr (is_vector_v<T>) {154 T out;155 // This for loop is optimized out for vector types.156 for (size_t i = 0; i < sizeof(T); ++i)157 out[i] = value;158 return out;159 }160}161 162///////////////////////////////////////////////////////////////////////////////163// Memset164///////////////////////////////////////////////////////////////////////////////165 166template <typename T> struct Memset {167 static_assert(is_element_type_v<T>);168 static constexpr size_t SIZE = sizeof(T);169 170 LIBC_INLINE static void block(Ptr dst, uint8_t value) {171 if constexpr (is_scalar_v<T> || is_vector_v<T>) {172 // Avoid ambiguous call due to ADL173 generic::store<T>(dst, splat<T>(value));174 } else if constexpr (is_array_v<T>) {175 using value_type = typename T::value_type;176 const auto Splat = splat<value_type>(value);177 for (size_t i = 0; i < array_size_v<T>; ++i)178 store<value_type>(dst + (i * sizeof(value_type)), Splat);179 }180 }181 182 LIBC_INLINE static void tail(Ptr dst, uint8_t value, size_t count) {183 block(dst + count - SIZE, value);184 }185 186 LIBC_INLINE static void head_tail(Ptr dst, uint8_t value, size_t count) {187 block(dst, value);188 tail(dst, value, count);189 }190 191 LIBC_INLINE static void loop_and_tail_offset(Ptr dst, uint8_t value,192 size_t count, size_t offset) {193 static_assert(SIZE > 1, "a loop of size 1 does not need tail");194 do {195 block(dst + offset, value);196 offset += SIZE;197 } while (offset < count - SIZE);198 tail(dst, value, count);199 }200 201 LIBC_INLINE static void loop_and_tail(Ptr dst, uint8_t value, size_t count) {202 return loop_and_tail_offset(dst, value, count, 0);203 }204};205 206template <typename T, typename... TS> struct MemsetSequence {207 static constexpr size_t SIZE = (sizeof(T) + ... + sizeof(TS));208 LIBC_INLINE static void block(Ptr dst, uint8_t value) {209 Memset<T>::block(dst, value);210 if constexpr (sizeof...(TS) > 0)211 return MemsetSequence<TS...>::block(dst + sizeof(T), value);212 }213};214 215///////////////////////////////////////////////////////////////////////////////216// Memmove217///////////////////////////////////////////////////////////////////////////////218 219template <typename T> struct Memmove {220 static_assert(is_element_type_v<T>);221 static constexpr size_t SIZE = sizeof(T);222 223 LIBC_INLINE static void block(Ptr dst, CPtr src) {224 store<T>(dst, load<T>(src));225 }226 227 LIBC_INLINE static void head_tail(Ptr dst, CPtr src, size_t count) {228 const size_t offset = count - SIZE;229 // The load and store operations can be performed in any order as long as230 // they are not interleaved. More investigations are needed to determine231 // the best order.232 const auto head = load<T>(src);233 const auto tail = load<T>(src + offset);234 store<T>(dst, head);235 store<T>(dst + offset, tail);236 }237 238 // Align forward suitable when dst < src. The alignment is performed with239 // an HeadTail operation of count ∈ [Alignment, 2 x Alignment].240 //241 // e.g. Moving two bytes forward, we make sure src is aligned.242 // [ | | | | ]243 // [____XXXXXXXXXXXXXXXXXXXXXXXXXXXX_]244 // [____LLLLLLLL_____________________]245 // [___________LLLLLLLA______________]246 // [_SSSSSSSS________________________]247 // [________SSSSSSSS_________________]248 //249 // e.g. Moving two bytes forward, we make sure dst is aligned.250 // [ | | | | ]251 // [____XXXXXXXXXXXXXXXXXXXXXXXXXXXX_]252 // [____LLLLLLLL_____________________]253 // [______LLLLLLLL___________________]254 // [_SSSSSSSS________________________]255 // [___SSSSSSSA______________________]256 template <Arg AlignOn>257 LIBC_INLINE static void align_forward(Ptr &dst, CPtr &src, size_t &count) {258 Ptr prev_dst = dst;259 CPtr prev_src = src;260 size_t prev_count = count;261 align_to_next_boundary<SIZE, AlignOn>(dst, src, count);262 adjust(SIZE, dst, src, count);263 head_tail(prev_dst, prev_src, prev_count - count);264 }265 266 // Align backward suitable when dst > src. The alignment is performed with267 // an HeadTail operation of count ∈ [Alignment, 2 x Alignment].268 //269 // e.g. Moving two bytes backward, we make sure src is aligned.270 // [ | | | | ]271 // [____XXXXXXXXXXXXXXXXXXXXXXXX_____]272 // [ _________________ALLLLLLL_______]273 // [ ___________________LLLLLLLL_____]274 // [____________________SSSSSSSS_____]275 // [______________________SSSSSSSS___]276 //277 // e.g. Moving two bytes backward, we make sure dst is aligned.278 // [ | | | | ]279 // [____XXXXXXXXXXXXXXXXXXXXXXXX_____]280 // [ _______________LLLLLLLL_________]281 // [ ___________________LLLLLLLL_____]282 // [__________________ASSSSSSS_______]283 // [______________________SSSSSSSS___]284 template <Arg AlignOn>285 LIBC_INLINE static void align_backward(Ptr &dst, CPtr &src, size_t &count) {286 Ptr headtail_dst = dst + count;287 CPtr headtail_src = src + count;288 size_t headtail_size = 0;289 align_to_next_boundary<SIZE, AlignOn>(headtail_dst, headtail_src,290 headtail_size);291 adjust(-2 * SIZE, headtail_dst, headtail_src, headtail_size);292 head_tail(headtail_dst, headtail_src, headtail_size);293 count -= headtail_size;294 }295 296 // Move forward suitable when dst < src. We load the tail bytes before297 // handling the loop.298 //299 // e.g. Moving two bytes300 // [ | | | | |]301 // [___XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX___]302 // [_________________________LLLLLLLL___]303 // [___LLLLLLLL_________________________]304 // [_SSSSSSSS___________________________]305 // [___________LLLLLLLL_________________]306 // [_________SSSSSSSS___________________]307 // [___________________LLLLLLLL_________]308 // [_________________SSSSSSSS___________]309 // [_______________________SSSSSSSS_____]310 LIBC_INLINE static void loop_and_tail_forward(Ptr dst, CPtr src,311 size_t count) {312 static_assert(SIZE > 1, "a loop of size 1 does not need tail");313 const size_t tail_offset = count - SIZE;314 const auto tail_value = load<T>(src + tail_offset);315 size_t offset = 0;316 LIBC_LOOP_NOUNROLL317 do {318 block(dst + offset, src + offset);319 offset += SIZE;320 } while (offset < count - SIZE);321 store<T>(dst + tail_offset, tail_value);322 }323 324 // Move backward suitable when dst > src. We load the head bytes before325 // handling the loop.326 //327 // e.g. Moving two bytes328 // [ | | | | |]329 // [___XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX___]330 // [___LLLLLLLL_________________________]331 // [_________________________LLLLLLLL___]332 // [___________________________SSSSSSSS_]333 // [_________________LLLLLLLL___________]334 // [___________________SSSSSSSS_________]335 // [_________LLLLLLLL___________________]336 // [___________SSSSSSSS_________________]337 // [_____SSSSSSSS_______________________]338 LIBC_INLINE static void loop_and_tail_backward(Ptr dst, CPtr src,339 size_t count) {340 static_assert(SIZE > 1, "a loop of size 1 does not need tail");341 const auto head_value = load<T>(src);342 ptrdiff_t offset = count - SIZE;343 LIBC_LOOP_NOUNROLL344 do {345 block(dst + offset, src + offset);346 offset -= SIZE;347 } while (offset >= 0);348 store<T>(dst, head_value);349 }350};351 352///////////////////////////////////////////////////////////////////////////////353// Low level operations for Bcmp and Memcmp that operate on memory locations.354///////////////////////////////////////////////////////////////////////////////355 356// Same as load above but with an offset to the pointer.357// Making the offset explicit hints the compiler to use relevant addressing mode358// consistently.359template <typename T> LIBC_INLINE T load(CPtr ptr, size_t offset) {360 return ::LIBC_NAMESPACE::load<T>(ptr + offset);361}362 363// Same as above but also makes sure the loaded value is in big endian format.364// This is useful when implementing lexicograhic comparisons as big endian365// scalar comparison directly maps to lexicographic byte comparisons.366template <typename T> LIBC_INLINE T load_be(CPtr ptr, size_t offset) {367 return Endian::to_big_endian(load<T>(ptr, offset));368}369 370// Equality: returns true iff values at locations (p1 + offset) and (p2 +371// offset) compare equal.372template <typename T> LIBC_INLINE bool eq(CPtr p1, CPtr p2, size_t offset);373 374// Not equals: returns non-zero iff values at locations (p1 + offset) and (p2 +375// offset) differ.376template <typename T> LIBC_INLINE uint32_t neq(CPtr p1, CPtr p2, size_t offset);377 378// Lexicographic comparison:379// - returns 0 iff values at locations (p1 + offset) and (p2 + offset) compare380// equal.381// - returns a negative value if value at location (p1 + offset) is382// lexicographically less than value at (p2 + offset).383// - returns a positive value if value at location (p1 + offset) is384// lexicographically greater than value at (p2 + offset).385template <typename T>386LIBC_INLINE MemcmpReturnType cmp(CPtr p1, CPtr p2, size_t offset);387 388// Lexicographic comparison of non-equal values:389// - returns a negative value if value at location (p1 + offset) is390// lexicographically less than value at (p2 + offset).391// - returns a positive value if value at location (p1 + offset) is392// lexicographically greater than value at (p2 + offset).393template <typename T>394LIBC_INLINE MemcmpReturnType cmp_neq(CPtr p1, CPtr p2, size_t offset);395 396///////////////////////////////////////////////////////////////////////////////397// Memcmp implementation398//399// When building memcmp, not all types are considered equals.400//401// For instance, the lexicographic comparison of two uint8_t can be implemented402// as a simple subtraction, but for wider operations the logic can be much more403// involving, especially on little endian platforms.404//405// For such wider types it is a good strategy to test for equality first and406// only do the expensive lexicographic comparison if necessary.407//408// Decomposing the algorithm like this for wider types allows us to have409// efficient implementation of higher order functions like 'head_tail' or410// 'loop_and_tail'.411///////////////////////////////////////////////////////////////////////////////412 413// Type traits to decide whether we can use 'cmp' directly or if we need to414// split the computation.415template <typename T> struct cmp_is_expensive;416 417template <typename T> struct Memcmp {418 static_assert(is_element_type_v<T>);419 static constexpr size_t SIZE = sizeof(T);420 421private:422 LIBC_INLINE static MemcmpReturnType block_offset(CPtr p1, CPtr p2,423 size_t offset) {424 if constexpr (cmp_is_expensive<T>::value) {425 if (!eq<T>(p1, p2, offset))426 return cmp_neq<T>(p1, p2, offset);427 return MemcmpReturnType::zero();428 } else {429 return cmp<T>(p1, p2, offset);430 }431 }432 433public:434 LIBC_INLINE static MemcmpReturnType block(CPtr p1, CPtr p2) {435 return block_offset(p1, p2, 0);436 }437 438 LIBC_INLINE static MemcmpReturnType tail(CPtr p1, CPtr p2, size_t count) {439 return block_offset(p1, p2, count - SIZE);440 }441 442 LIBC_INLINE static MemcmpReturnType head_tail(CPtr p1, CPtr p2,443 size_t count) {444 if constexpr (cmp_is_expensive<T>::value) {445 if (!eq<T>(p1, p2, 0))446 return cmp_neq<T>(p1, p2, 0);447 } else {448 if (const auto value = cmp<T>(p1, p2, 0))449 return value;450 }451 return tail(p1, p2, count);452 }453 454 LIBC_INLINE static MemcmpReturnType loop_and_tail(CPtr p1, CPtr p2,455 size_t count) {456 return loop_and_tail_offset(p1, p2, count, 0);457 }458 459 LIBC_INLINE static MemcmpReturnType460 loop_and_tail_offset(CPtr p1, CPtr p2, size_t count, size_t offset) {461 if constexpr (SIZE > 1) {462 const size_t limit = count - SIZE;463 LIBC_LOOP_NOUNROLL464 for (; offset < limit; offset += SIZE) {465 if constexpr (cmp_is_expensive<T>::value) {466 if (!eq<T>(p1, p2, offset))467 return cmp_neq<T>(p1, p2, offset);468 } else {469 if (const auto value = cmp<T>(p1, p2, offset))470 return value;471 }472 }473 return block_offset(p1, p2, limit); // tail474 } else {475 // No need for a tail operation when SIZE == 1.476 LIBC_LOOP_NOUNROLL477 for (; offset < count; offset += SIZE)478 if (auto value = cmp<T>(p1, p2, offset))479 return value;480 return MemcmpReturnType::zero();481 }482 }483 484 LIBC_INLINE static MemcmpReturnType485 loop_and_tail_align_above(size_t threshold, CPtr p1, CPtr p2, size_t count) {486 const AlignHelper<sizeof(T)> helper(p1);487 if (LIBC_UNLIKELY(count >= threshold) && helper.not_aligned()) {488 if (auto value = block(p1, p2))489 return value;490 adjust(helper.offset, p1, p2, count);491 }492 return loop_and_tail(p1, p2, count);493 }494};495 496template <typename T, typename... TS> struct MemcmpSequence {497 static constexpr size_t SIZE = (sizeof(T) + ... + sizeof(TS));498 LIBC_INLINE static MemcmpReturnType block(CPtr p1, CPtr p2) {499 // TODO: test suggestion in500 // https://reviews.llvm.org/D148717?id=515724#inline-1446890501 // once we have a proper way to check memory operation latency.502 if constexpr (cmp_is_expensive<T>::value) {503 if (!eq<T>(p1, p2, 0))504 return cmp_neq<T>(p1, p2, 0);505 } else {506 if (auto value = cmp<T>(p1, p2, 0))507 return value;508 }509 if constexpr (sizeof...(TS) > 0)510 return MemcmpSequence<TS...>::block(p1 + sizeof(T), p2 + sizeof(T));511 else512 return MemcmpReturnType::zero();513 }514};515 516///////////////////////////////////////////////////////////////////////////////517// Bcmp518///////////////////////////////////////////////////////////////////////////////519template <typename T> struct Bcmp {520 static_assert(is_element_type_v<T>);521 static constexpr size_t SIZE = sizeof(T);522 523 LIBC_INLINE static BcmpReturnType block(CPtr p1, CPtr p2) {524 return neq<T>(p1, p2, 0);525 }526 527 LIBC_INLINE static BcmpReturnType tail(CPtr p1, CPtr p2, size_t count) {528 const size_t tail_offset = count - SIZE;529 return neq<T>(p1, p2, tail_offset);530 }531 532 LIBC_INLINE static BcmpReturnType head_tail(CPtr p1, CPtr p2, size_t count) {533 if (const auto value = neq<T>(p1, p2, 0))534 return value;535 return tail(p1, p2, count);536 }537 538 LIBC_INLINE static BcmpReturnType loop_and_tail(CPtr p1, CPtr p2,539 size_t count) {540 return loop_and_tail_offset(p1, p2, count, 0);541 }542 543 LIBC_INLINE static BcmpReturnType544 loop_and_tail_offset(CPtr p1, CPtr p2, size_t count, size_t offset) {545 if constexpr (SIZE > 1) {546 const size_t limit = count - SIZE;547 LIBC_LOOP_NOUNROLL548 for (; offset < limit; offset += SIZE)549 if (const auto value = neq<T>(p1, p2, offset))550 return value;551 return tail(p1, p2, count);552 } else {553 // No need for a tail operation when SIZE == 1.554 LIBC_LOOP_NOUNROLL555 for (; offset < count; offset += SIZE)556 if (const auto value = neq<T>(p1, p2, offset))557 return value;558 return BcmpReturnType::zero();559 }560 }561 562 LIBC_INLINE static BcmpReturnType563 loop_and_tail_align_above(size_t threshold, CPtr p1, CPtr p2, size_t count) {564 static_assert(SIZE > 1,565 "No need to align when processing one byte at a time");566 const AlignHelper<sizeof(T)> helper(p1);567 if (LIBC_UNLIKELY(count >= threshold) && helper.not_aligned()) {568 if (auto value = block(p1, p2))569 return value;570 adjust(helper.offset, p1, p2, count);571 }572 return loop_and_tail(p1, p2, count);573 }574};575 576template <typename T, typename... TS> struct BcmpSequence {577 static constexpr size_t SIZE = (sizeof(T) + ... + sizeof(TS));578 LIBC_INLINE static BcmpReturnType block(CPtr p1, CPtr p2) {579 if (auto value = neq<T>(p1, p2, 0))580 return value;581 if constexpr (sizeof...(TS) > 0)582 return BcmpSequence<TS...>::block(p1 + sizeof(T), p2 + sizeof(T));583 else584 return BcmpReturnType::zero();585 }586};587 588///////////////////////////////////////////////////////////////////////////////589// Specializations for uint8_t590template <> struct cmp_is_expensive<uint8_t> : public cpp::false_type {};591template <> LIBC_INLINE bool eq<uint8_t>(CPtr p1, CPtr p2, size_t offset) {592 return load<uint8_t>(p1, offset) == load<uint8_t>(p2, offset);593}594template <> LIBC_INLINE uint32_t neq<uint8_t>(CPtr p1, CPtr p2, size_t offset) {595 return load<uint8_t>(p1, offset) ^ load<uint8_t>(p2, offset);596}597template <>598LIBC_INLINE MemcmpReturnType cmp<uint8_t>(CPtr p1, CPtr p2, size_t offset) {599 return static_cast<int32_t>(load<uint8_t>(p1, offset)) -600 static_cast<int32_t>(load<uint8_t>(p2, offset));601}602template <>603LIBC_INLINE MemcmpReturnType cmp_neq<uint8_t>(CPtr p1, CPtr p2, size_t offset);604 605} // namespace generic606} // namespace LIBC_NAMESPACE_DECL607 608#endif // LLVM_LIBC_SRC_STRING_MEMORY_UTILS_OP_GENERIC_H609