1 /**
2 * Copyright (c) 2024 Huawei Device Co., Ltd.
3 * Licensed under the Apache License, Version 2.0 (the "License");
4 * you may not use this file except in compliance with the License.
5 * You may obtain a copy of the License at
6 *
7 * http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software
10 * distributed under the License is distributed on an "AS IS" BASIS,
11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 * See the License for the specific language governing permissions and
13 * limitations under the License.
14 */
15
16 #include <charconv>
17 #include "ets_class_linker_extension.h"
18 #include "ets_to_string_cache.h"
19 #include "ets_intrinsics_helpers.h"
20
21 namespace ark::ets::detail {
22
23 template <typename T>
24 class EtsToStringCacheElement : public EtsObject {
25 public:
26 ~EtsToStringCacheElement() = default;
27 NO_COPY_SEMANTIC(EtsToStringCacheElement);
28 NO_MOVE_SEMANTIC(EtsToStringCacheElement);
29
30 using Flag = uint32_t;
31 static constexpr Flag FLAG_MASK = 1U;
32
33 static EtsClass *GetClass(EtsCoroutine *coro);
34
35 static EtsToStringCacheElement<T> *Create(EtsCoroutine *coro, EtsHandle<EtsString> &stringHandle, T number,
36 EtsClass *klass);
37
FromCoreType(ObjectHeader * obj)38 static EtsToStringCacheElement<T> *FromCoreType(ObjectHeader *obj)
39 {
40 return reinterpret_cast<EtsToStringCacheElement<T> *>(obj);
41 }
42
GetCoreType()43 ObjectHeader *GetCoreType()
44 {
45 return reinterpret_cast<ObjectHeader *>(this);
46 }
47
AsObject()48 EtsObject *AsObject()
49 {
50 return this;
51 }
52
LoadNumber() const53 T LoadNumber() const
54 {
55 T result;
56 // Atomic with acquire order reason: make `flag` writes from other threads visible
57 // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg)
58 __atomic_load(&number_, &result, __ATOMIC_ACQUIRE);
59 return result;
60 }
61
62 /*
63 * `Data` contains pointer to managed `string` representing corresponding `number` and int `flag` used to provide
64 * atomicity `flag` is incremented by 1 in the beginning and in the end of cache update, so:
65 * - if we read odd `flag` from another thread, we consider the cache element "locked" and don't use it for
66 * read/write
67 * - if `flag` is different before and after read of `number`, the cache element is being updated, and we ignore it
68 * `Data` is aligned by 8 bytes to read `string` and `flag` with a single atomic operation
69 */
70 struct alignas(alignof(uint64_t)) Data {
71 ObjectPointer<EtsString> string {};
72 Flag flag {};
73 };
74 static_assert(sizeof(Data) == sizeof(uint64_t));
75 static_assert(std::atomic<Data>::is_always_lock_free);
76
LoadData() const77 Data LoadData() const
78 {
79 Data result;
80 // Atomic with acquire order reason: make `number` and `flag` writes from other threads visible
81 // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg)
82 __atomic_load(&data_, &result, __ATOMIC_ACQUIRE);
83 return result;
84 }
85
IsLocked(Flag flag)86 static bool IsLocked(Flag flag)
87 {
88 return (flag & FLAG_MASK) != 0;
89 }
90
IsFresh(Flag flag) const91 bool IsFresh(Flag flag) const
92 {
93 // Atomic with relaxed order reason: used only after acquire-loads
94 // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg)
95 auto newFlag = __atomic_load_n(&data_.flag, __ATOMIC_RELAXED);
96 return newFlag == flag;
97 }
98
99 /*
100 * Reasoning on correctness:
101 * If compare-exchange acq_rel w1 succeeds, than it sees result of w3 (from this or other writer thread),
102 * because w1 makes `flag` even and `w3` makes it odd.
103 * so ... w3 HB (w1 HB w2 HB w3) HB w1 ..., and there is a total order on all writes to `elem`
104 */
TryStore(EtsCoroutine * coro,EtsString * string,T number,Data oldData)105 ToStringResult TryStore(EtsCoroutine *coro, EtsString *string, T number, Data oldData)
106 {
107 ASSERT(!IsLocked(oldData.flag));
108 Data newData {string, oldData.flag + FLAG_MASK};
109 auto *barrierSet = coro->GetBarrierSet();
110
111 if (UNLIKELY(barrierSet->IsPreBarrierEnabled())) {
112 auto *oldValue = ObjectAccessor::GetObject(this, STRING_OFFSET);
113 barrierSet->PreBarrier(oldValue);
114 }
115 auto oldCopy = oldData;
116 // Atomic with acquire order reason: sync flag
117 while (
118 // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg)
119 !__atomic_compare_exchange(&data_, &oldData, &newData, true, __ATOMIC_ACQUIRE, __ATOMIC_RELAXED)) { // w1
120 if (bit_cast<uint64_t>(oldCopy) == bit_cast<uint64_t>(oldData)) {
121 // spurious failure, try compare_exchange again
122 continue;
123 }
124 return ToStringResult::STORE_FAIL;
125 }
126 // Atomic with release order reason: make flag update visible in other threads
127 // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg)
128 __atomic_store(&number_, &number, __ATOMIC_RELEASE); // w2
129 oldData = newData;
130 newData.flag++;
131 // Atomic with release order reason: number write must be visible after flag update
132 // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg)
133 while (!__atomic_compare_exchange(&data_, &oldData, &newData, true, __ATOMIC_RELEASE, __ATOMIC_RELAXED)) {
134 } // w3
135 ASSERT(string != nullptr);
136 if (!mem::IsEmptyBarrier(barrierSet->GetPostType())) {
137 barrierSet->PostBarrier(this, STRING_OFFSET, string);
138 }
139 return ToStringResult::STORE_UPDATE;
140 }
141
GetUnalignedSize()142 constexpr static size_t GetUnalignedSize()
143 {
144 return NUMBER_OFFSET + sizeof(T);
145 }
146
147 private:
148 void SetString(EtsCoroutine *coro, EtsString *string);
149
SetNumber(T number)150 void SetNumber(T number)
151 {
152 // Atomic with relaxed order reason: used only on newly-created object before store-release into array
153 ObjectAccessor::SetPrimitive(this, NUMBER_OFFSET, number);
154 }
155
156 private:
157 Data data_;
158
159 T number_;
160 constexpr static size_t STRING_OFFSET =
161 MEMBER_OFFSET(EtsToStringCacheElement<T>, data_) + MEMBER_OFFSET(Data, string);
162 [[maybe_unused]] constexpr static size_t FLAG_OFFSET =
163 MEMBER_OFFSET(EtsToStringCacheElement<T>, data_) + MEMBER_OFFSET(Data, flag);
164 constexpr static size_t NUMBER_OFFSET = MEMBER_OFFSET(EtsToStringCacheElement<T>, number_);
165
166 friend class ark::ets::test::EtsToStringCacheTest;
167 };
168
169 /* static */
170 template <typename T>
GetClass(EtsCoroutine * coro)171 EtsClass *EtsToStringCacheElement<T>::GetClass(EtsCoroutine *coro)
172 {
173 auto *classLinker = coro->GetPandaVM()->GetClassLinker();
174 auto *ext = classLinker->GetEtsClassLinkerExtension();
175
176 std::string_view classDescriptor;
177 if constexpr (std::is_same_v<T, EtsDouble>) {
178 classDescriptor = panda_file_items::class_descriptors::DOUBLE_TO_STRING_CACHE_ELEMENT;
179 } else if constexpr (std::is_same_v<T, EtsFloat>) {
180 classDescriptor = panda_file_items::class_descriptors::FLOAT_TO_STRING_CACHE_ELEMENT;
181 } else if constexpr (std::is_same_v<T, EtsLong>) {
182 classDescriptor = panda_file_items::class_descriptors::LONG_TO_STRING_CACHE_ELEMENT;
183 } else {
184 UNREACHABLE();
185 }
186 return classLinker->GetClass(classDescriptor.data(), false, ext->GetBootContext());
187 }
188
189 /* static */
190 template <typename T>
Create(EtsCoroutine * coro,EtsHandle<EtsString> & stringHandle,T number,EtsClass * klass)191 EtsToStringCacheElement<T> *EtsToStringCacheElement<T>::Create(EtsCoroutine *coro, EtsHandle<EtsString> &stringHandle,
192 T number, EtsClass *klass)
193 {
194 static_assert(STRING_OFFSET == sizeof(uint64_t));
195 static_assert(NUMBER_OFFSET == sizeof(uint64_t) * 2U);
196 auto *instance = FromCoreType(EtsObject::Create(coro, klass)->GetCoreType());
197 instance->SetString(coro, stringHandle.GetPtr());
198 instance->SetNumber(number);
199 return instance;
200 }
201
202 template <typename T>
SetString(EtsCoroutine * coro,EtsString * string)203 void EtsToStringCacheElement<T>::SetString(EtsCoroutine *coro, EtsString *string)
204 {
205 static_assert(STRING_OFFSET == 8U);
206 // Atomic with relaxed order reason: used only on newly-created object before store-release into array
207 ObjectAccessor::SetObject(coro, this, STRING_OFFSET, string->GetCoreType());
208 }
209
210 template <typename T>
ToString(T number)211 EtsString *ToString(T number)
212 {
213 if constexpr (std::is_same_v<T, EtsLong>) {
214 // 1 for first digit and 1 for zero
215 constexpr auto MAX_DIGITS = std::numeric_limits<int64_t>::digits10 + 2U;
216 // NOLINTNEXTLINE(cppcoreguidelines-pro-type-member-init)
217 std::array<char, MAX_DIGITS + 1> buf;
218 auto [stringEnd, result] = std::to_chars(buf.begin(), buf.end(), number);
219 ASSERT(result == std::errc());
220 return EtsString::CreateFromAscii(buf.begin(), stringEnd - buf.begin());
221 } else {
222 static_assert(std::is_floating_point_v<T>);
223 return intrinsics::helpers::FpToStringDecimalRadix(
224 number, [](std::string_view str) { return EtsString::CreateFromAscii(str.data(), str.length()); });
225 }
226 }
227
228 template <typename T>
229 struct SimpleHash {
230 static constexpr uint32_t CACHE_SIZE_SHIFT = 8U;
231 static constexpr uint32_t T_BITS = sizeof(T) * BITS_PER_BYTE;
232 using UnsignedIntType = ark::helpers::TypeHelperT<T_BITS, false>;
233 static constexpr uint32_t SHIFT = T_BITS - CACHE_SIZE_SHIFT;
234
GetMulark::ets::detail::SimpleHash235 static constexpr UnsignedIntType GetMul()
236 {
237 if constexpr (std::is_same_v<UnsignedIntType, uint32_t>) {
238 constexpr auto MUL = 0x5bd1e995U;
239 return MUL;
240 } else {
241 constexpr auto MUL = 0xc6a4a7935bd1e995ULL;
242 return MUL;
243 }
244 }
245
operator ()ark::ets::detail::SimpleHash246 size_t operator()(T number) const
247 {
248 auto res = (bit_cast<UnsignedIntType>(number) * GetMul()) >> SHIFT;
249 ASSERT(res < (1U << CACHE_SIZE_SHIFT));
250 return static_cast<size_t>(res);
251 }
252 };
253
254 /* static */
255 template <typename T, typename Derived, typename Hash>
GetIndex(T number)256 uint32_t EtsToStringCache<T, Derived, Hash>::GetIndex(T number)
257 {
258 auto index = Hash()(number);
259 ASSERT(index < SIZE);
260 return index;
261 }
262
263 template <typename T, typename Derived, typename Hash>
FinishUpdate(EtsCoroutine * coro,T number,EtsToStringCacheElement<T> * elem,uint64_t cachedAsInt)264 std::pair<EtsString *, ToStringResult> EtsToStringCache<T, Derived, Hash>::FinishUpdate(
265 EtsCoroutine *coro, T number, EtsToStringCacheElement<T> *elem, uint64_t cachedAsInt)
266 {
267 [[maybe_unused]] EtsHandleScope scope(coro);
268 EtsHandle<EtsToStringCacheElement<T>> elemHandle(coro, elem);
269 // may trigger GC
270 auto *string = ToString(number);
271 ASSERT(string != nullptr);
272 auto *cached = reinterpret_cast<typename EtsToStringCacheElement<T>::Data *>(&cachedAsInt);
273 auto storeRes = elemHandle->TryStore(coro, string, number, *cached);
274 return {string, storeRes};
275 }
276
277 // Reasoning on correctness:
278 // If we return cached `number`, r1 and r3 saw the same `flag` stored by `w1` or `w3` (I) (or initial `0` value as
279 // corner-case) It cannot be `w1`, because `w1` stores odd flag, and we have read even flag. So, `w2` HB `w3`
280 // HB(rel-acq) `r1` HB `r2` HB `r3`, and `w2` HB `r2`.
281 // Consider the case when `r2` sees some later write to number, `w2a`. Then `w1a` HB `w2a` HB(rel-acq) `r2` HB `r3`,
282 // and `r3` sees `w1a` or some later write to `flag`, which leads to contradiction with (I). (we consider equality
283 // after overflow impossible, because in this case writer threads do ~2^32 operations while the reader does nothing).
284 // So `r2` sees number from `w2`.
285 // `r1` sees string from `w1` because of (I), so we prove that string and number in successful cache read correspond to
286 // each other.
287 //
288 // In case of cache miss which leads to store, ordering of `r2` and `r3` does not matter because we only use `r1` and
289 // pass it to compare-exchange.
290 template <typename T, typename Derived, typename Hash>
GetOrCacheImpl(EtsCoroutine * coro,T number)291 std::pair<EtsString *, ToStringResult> EtsToStringCache<T, Derived, Hash>::GetOrCacheImpl(EtsCoroutine *coro, T number)
292 {
293 auto index = GetIndex(number);
294 // Atomic with acquire order reason: write of `elem` to array is dependency-ordered before reads from loaded `elem`
295 auto *elem = Base::Get(index, std::memory_order_acquire);
296 if (UNLIKELY(elem == nullptr)) {
297 [[maybe_unused]] EtsHandleScope scope(coro);
298 EtsHandle<EtsString> string(coro, ToString(number));
299 ASSERT(string.GetPtr() != nullptr);
300 // may trigger GC
301 StoreToCache(coro, string, number, index);
302 ASSERT(string.GetPtr() != nullptr);
303 return {string.GetPtr(), ToStringResult::STORE_NEW};
304 }
305 auto cached = elem->LoadData(); // r1
306 if (UNLIKELY(Elem::IsLocked(cached.flag))) {
307 return {ToString(number), ToStringResult::LOAD_FAIL_LOCKED};
308 }
309 auto cachedNumber = elem->LoadNumber(); // r2
310 if (LIKELY(cachedNumber == number)) {
311 if (LIKELY(elem->IsFresh(cached.flag))) { // r3
312 return {cached.string, ToStringResult::LOAD_CACHED};
313 }
314 return {ToString(number), ToStringResult::LOAD_FAIL_UPDATED};
315 }
316 return FinishUpdate(coro, number, elem, bit_cast<uint64_t>(cached));
317 }
318
319 template <typename T, typename Derived, typename Hash>
CacheAndGetNoCheck(EtsCoroutine * coro,T number,ObjectHeader * elem,uint64_t cached)320 EtsString *EtsToStringCache<T, Derived, Hash>::CacheAndGetNoCheck(EtsCoroutine *coro, T number, ObjectHeader *elem,
321 uint64_t cached)
322 {
323 ASSERT(elem != nullptr);
324 return FinishUpdate(coro, number, EtsToStringCacheElement<T>::FromCoreType(elem), cached).first;
325 }
326
327 /* static */
328 template <typename T, typename Derived, typename Hash>
GetNoCache(T number)329 EtsString *EtsToStringCache<T, Derived, Hash>::GetNoCache(T number)
330 {
331 return ToString(number);
332 }
333
334 /* static */
335 template <typename T, typename Derived, typename Hash>
Create(EtsCoroutine * coro)336 Derived *EtsToStringCache<T, Derived, Hash>::Create(EtsCoroutine *coro)
337 {
338 auto *etsClass = detail::EtsToStringCacheElement<T>::GetClass(coro);
339 if (etsClass == nullptr) {
340 return nullptr;
341 }
342 return static_cast<Derived *>(Base::Create(etsClass, SIZE, SpaceType::SPACE_TYPE_NON_MOVABLE_OBJECT));
343 }
344
345 template <typename T, typename Derived, typename Hash>
StoreToCache(EtsCoroutine * coro,EtsHandle<EtsString> & stringHandle,T number,uint32_t index)346 void EtsToStringCache<T, Derived, Hash>::StoreToCache(EtsCoroutine *coro, EtsHandle<EtsString> &stringHandle, T number,
347 uint32_t index)
348 {
349 auto *arrayClass = Base::GetCoreType()->template ClassAddr<Class>();
350 auto *elemClass = arrayClass->GetComponentType();
351 ASSERT(elemClass->GetObjectSize() == EtsToStringCacheElement<T>::GetUnalignedSize());
352 auto *elem = EtsToStringCacheElement<T>::Create(coro, stringHandle, number, EtsClass::FromRuntimeClass(elemClass));
353 // Atomic with release order reason: writes to elem should be visible in other threads
354 Base::Set(index, elem, std::memory_order_release);
355 }
356
357 template class EtsToStringCache<EtsDouble, DoubleToStringCache>;
358 template class EtsToStringCache<EtsFloat, FloatToStringCache>;
359 template class EtsToStringCache<EtsLong, LongToStringCache>;
360
361 } // namespace ark::ets::detail
362