1 /**
2 * Copyright (c) 2024-2025 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 #include "libpandabase/mem/mem.h"
21
22 namespace ark::ets::detail {
23
24 template <typename T>
25 class EtsToStringCacheElement : public EtsObject {
26 public:
27 ~EtsToStringCacheElement() = default;
28 NO_COPY_SEMANTIC(EtsToStringCacheElement);
29 NO_MOVE_SEMANTIC(EtsToStringCacheElement);
30
31 using Flag = ObjectPointerType;
32 static constexpr Flag FLAG_MASK = 1U;
33
34 static EtsClass *GetClass(EtsCoroutine *coro);
35
36 static EtsToStringCacheElement<T> *Create(EtsCoroutine *coro, EtsHandle<EtsString> &stringHandle, T number,
37 EtsClass *klass);
38
FromCoreType(ObjectHeader * obj)39 static EtsToStringCacheElement<T> *FromCoreType(ObjectHeader *obj)
40 {
41 return reinterpret_cast<EtsToStringCacheElement<T> *>(obj);
42 }
43
GetCoreType()44 ObjectHeader *GetCoreType()
45 {
46 return reinterpret_cast<ObjectHeader *>(this);
47 }
48
AsObject()49 EtsObject *AsObject()
50 {
51 return this;
52 }
53
LoadNumber() const54 T LoadNumber() const
55 {
56 // Atomic with acquire order reason: make `flag` writes from other threads visible
57 // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg)
58 return AtomicLoad(&number_, std::memory_order_acquire);
59 }
60
61 /*
62 * `Data` contains pointer to managed `string` representing corresponding `number` and int `flag` used to provide
63 * atomicity `flag` is incremented by 1 in the beginning and in the end of cache update, so:
64 * - if we read odd `flag` from another thread, we consider the cache element "locked" and don't use it for
65 * read/write
66 * - if `flag` is different before and after read of `number`, the cache element is being updated, and we ignore it
67 * `Data` is aligned by 8 bytes to read `string` and `flag` with a single atomic operation
68 */
69 struct alignas(alignof(uint64_t)) Data {
70 ObjectPointer<EtsString> string {};
71 Flag flag {};
72 };
73 static_assert(sizeof(Data) == sizeof(ObjectPointerType) * 2U);
74 // NOTE(ipetrov): hack for 128 bit ObjectHeader
75 #if !defined(ARK_HYBRID)
76 static_assert(std::atomic<Data>::is_always_lock_free);
77 #endif
78
LoadData() const79 Data LoadData() const
80 {
81 // Atomic with acquire order reason: make `number` and `flag` writes from other threads visible
82 // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg)
83 return AtomicLoad(&data_, std::memory_order_acquire);
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 Flag newFlag = AtomicLoad(&data_.flag, std::memory_order_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 ASSERT(coro != nullptr);
110 auto *barrierSet = coro->GetBarrierSet();
111
112 if (UNLIKELY(barrierSet->IsPreBarrierEnabled())) {
113 auto *oldValue = ObjectAccessor::GetObject(this, STRING_OFFSET);
114 barrierSet->PreBarrier(oldValue);
115 }
116 auto oldCopy = oldData;
117 // Atomic with acquire order reason: sync flag
118 while (
119 // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg)
120 !AtomicCmpxchgWeak(&data_, oldData, newData, std::memory_order_acquire, std::memory_order_relaxed)) { // w1
121 if (oldCopy.string == oldData.string && oldCopy.flag == oldData.flag) {
122 // spurious failure, try compare_exchange again
123 continue;
124 }
125 return ToStringResult::STORE_FAIL;
126 }
127 // Atomic with release order reason: make flag update visible in other threads
128 // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg)
129 AtomicStore(&number_, number, std::memory_order_release); // w2
130 oldData = newData;
131 newData.flag++;
132 // Atomic with release order reason: number write must be visible after flag update
133 // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg)
134 while (!AtomicCmpxchgWeak(&data_, oldData, newData, std::memory_order_release, std::memory_order_relaxed)) {
135 } // w3
136 ASSERT(string != nullptr);
137 if (!mem::IsEmptyBarrier(barrierSet->GetPostType())) {
138 barrierSet->PostBarrier(this, STRING_OFFSET, string);
139 }
140 return ToStringResult::STORE_UPDATE;
141 }
142
GetUnalignedSize()143 constexpr static size_t GetUnalignedSize()
144 {
145 return NUMBER_OFFSET + sizeof(T);
146 }
147
148 private:
149 void SetString(EtsCoroutine *coro, EtsString *string);
150
SetNumber(T number)151 void SetNumber(T number)
152 {
153 // Atomic with relaxed order reason: used only on newly-created object before store-release into array
154 ObjectAccessor::SetPrimitive(this, NUMBER_OFFSET, number);
155 }
156
157 private:
158 Data data_;
159
160 T number_;
161 constexpr static size_t STRING_OFFSET =
162 MEMBER_OFFSET(EtsToStringCacheElement<T>, data_) + MEMBER_OFFSET(Data, string);
163 [[maybe_unused]] constexpr static size_t FLAG_OFFSET =
164 MEMBER_OFFSET(EtsToStringCacheElement<T>, data_) + MEMBER_OFFSET(Data, flag);
165 constexpr static size_t NUMBER_OFFSET = MEMBER_OFFSET(EtsToStringCacheElement<T>, number_);
166
167 friend class ark::ets::test::EtsToStringCacheTest;
168 };
169
170 /* static */
171 template <typename T>
GetClass(EtsCoroutine * coro)172 EtsClass *EtsToStringCacheElement<T>::GetClass(EtsCoroutine *coro)
173 {
174 auto *classLinker = coro->GetPandaVM()->GetClassLinker();
175 auto *ext = classLinker->GetEtsClassLinkerExtension();
176
177 std::string_view classDescriptor;
178 if constexpr (std::is_same_v<T, EtsDouble>) {
179 classDescriptor = panda_file_items::class_descriptors::DOUBLE_TO_STRING_CACHE_ELEMENT;
180 } else if constexpr (std::is_same_v<T, EtsFloat>) {
181 classDescriptor = panda_file_items::class_descriptors::FLOAT_TO_STRING_CACHE_ELEMENT;
182 } else if constexpr (std::is_same_v<T, EtsLong>) {
183 classDescriptor = panda_file_items::class_descriptors::LONG_TO_STRING_CACHE_ELEMENT;
184 } else {
185 UNREACHABLE();
186 }
187 return classLinker->GetClass(classDescriptor.data(), false, ext->GetBootContext());
188 }
189
190 /* static */
191 template <typename T>
Create(EtsCoroutine * coro,EtsHandle<EtsString> & stringHandle,T number,EtsClass * klass)192 EtsToStringCacheElement<T> *EtsToStringCacheElement<T>::Create(EtsCoroutine *coro, EtsHandle<EtsString> &stringHandle,
193 T number, EtsClass *klass)
194 {
195 static_assert(STRING_OFFSET == sizeof(ObjectPointerType) * 2U);
196 static_assert(NUMBER_OFFSET == sizeof(ObjectPointerType) * 2U + sizeof(Data));
197 auto *instance = FromCoreType(EtsObject::Create(coro, klass)->GetCoreType());
198 instance->SetString(coro, stringHandle.GetPtr());
199 instance->SetNumber(number);
200 return instance;
201 }
202
203 template <typename T>
SetString(EtsCoroutine * coro,EtsString * string)204 void EtsToStringCacheElement<T>::SetString(EtsCoroutine *coro, EtsString *string)
205 {
206 static_assert(STRING_OFFSET == sizeof(ObjectPointerType) * 2U);
207 ASSERT(string != nullptr);
208 // Atomic with relaxed order reason: used only on newly-created object before store-release into array
209 ObjectAccessor::SetObject(coro, this, STRING_OFFSET, string->GetCoreType());
210 }
211
212 template <typename T>
ToString(T number)213 EtsString *ToString(T number)
214 {
215 if constexpr (std::is_same_v<T, EtsLong>) {
216 // 1 for first digit and 1 for zero
217 constexpr auto MAX_DIGITS = std::numeric_limits<int64_t>::digits10 + 2U;
218 // NOLINTNEXTLINE(cppcoreguidelines-pro-type-member-init)
219 std::array<char, MAX_DIGITS + 1> buf;
220 auto [stringEnd, result] = std::to_chars(buf.begin(), buf.end(), number);
221 ASSERT(result == std::errc());
222 return EtsString::CreateFromAscii(buf.begin(), stringEnd - buf.begin());
223 } else {
224 static_assert(std::is_floating_point_v<T>);
225 return intrinsics::helpers::FpToStringDecimalRadix(
226 number, [](std::string_view str) { return EtsString::CreateFromAscii(str.data(), str.length()); });
227 }
228 }
229
230 template <typename T>
231 struct SimpleHash {
232 static constexpr uint32_t CACHE_SIZE_SHIFT = 8U;
233 static constexpr uint32_t T_BITS = sizeof(T) * BITS_PER_BYTE;
234 using UnsignedIntType = ark::helpers::TypeHelperT<T_BITS, false>;
235 static constexpr uint32_t SHIFT = T_BITS - CACHE_SIZE_SHIFT;
236
GetMulark::ets::detail::SimpleHash237 static constexpr UnsignedIntType GetMul()
238 {
239 if constexpr (std::is_same_v<UnsignedIntType, uint32_t>) {
240 constexpr auto MUL = 0x5bd1e995U;
241 return MUL;
242 } else {
243 constexpr auto MUL = 0xc6a4a7935bd1e995ULL;
244 return MUL;
245 }
246 }
247
operator ()ark::ets::detail::SimpleHash248 size_t operator()(T number) const
249 {
250 auto res = (bit_cast<UnsignedIntType>(number) * GetMul()) >> SHIFT;
251 ASSERT(res < (1U << CACHE_SIZE_SHIFT));
252 return static_cast<size_t>(res);
253 }
254 };
255
256 /* static */
257 template <typename T, typename Derived, typename Hash>
GetIndex(T number)258 uint32_t EtsToStringCache<T, Derived, Hash>::GetIndex(T number)
259 {
260 auto index = Hash()(number);
261 ASSERT(index < SIZE);
262 return index;
263 }
264
265 template <typename T, typename Derived, typename Hash>
FinishUpdate(EtsCoroutine * coro,T number,EtsToStringCacheElement<T> * elem,uint64_t cachedAsInt)266 std::pair<EtsString *, ToStringResult> EtsToStringCache<T, Derived, Hash>::FinishUpdate(
267 EtsCoroutine *coro, T number, EtsToStringCacheElement<T> *elem, uint64_t cachedAsInt)
268 {
269 // NOTE(ipetrov): hack for 128 bit ObjectHeader
270 #if defined(PANDA_TARGET_64) && !defined(PANDA_USE_32_BIT_POINTER)
271 UNUSED_VAR(coro);
272 UNUSED_VAR(elem);
273 UNUSED_VAR(cachedAsInt);
274 return {ToString(number), ToStringResult::STORE_UPDATE};
275 #else
276 [[maybe_unused]] EtsHandleScope scope(coro);
277 EtsHandle<EtsToStringCacheElement<T>> elemHandle(coro, elem);
278 // may trigger GC
279 auto *string = ToString(number);
280 ASSERT(string != nullptr);
281 auto *cached = reinterpret_cast<typename EtsToStringCacheElement<T>::Data *>(&cachedAsInt);
282 ASSERT(elemHandle.GetPtr() != nullptr);
283 auto storeRes = elemHandle->TryStore(coro, string, number, *cached);
284 return {string, storeRes};
285 #endif
286 }
287
288 // Reasoning on correctness:
289 // If we return cached `number`, r1 and r3 saw the same `flag` stored by `w1` or `w3` (I) (or initial `0` value as
290 // corner-case) It cannot be `w1`, because `w1` stores odd flag, and we have read even flag. So, `w2` HB `w3`
291 // HB(rel-acq) `r1` HB `r2` HB `r3`, and `w2` HB `r2`.
292 // Consider the case when `r2` sees some later write to number, `w2a`. Then `w1a` HB `w2a` HB(rel-acq) `r2` HB `r3`,
293 // and `r3` sees `w1a` or some later write to `flag`, which leads to contradiction with (I). (we consider equality
294 // after overflow impossible, because in this case writer threads do ~2^32 operations while the reader does nothing).
295 // So `r2` sees number from `w2`.
296 // `r1` sees string from `w1` because of (I), so we prove that string and number in successful cache read correspond to
297 // each other.
298 //
299 // In case of cache miss which leads to store, ordering of `r2` and `r3` does not matter because we only use `r1` and
300 // pass it to compare-exchange.
301 template <typename T, typename Derived, typename Hash>
GetOrCacheImpl(EtsCoroutine * coro,T number)302 std::pair<EtsString *, ToStringResult> EtsToStringCache<T, Derived, Hash>::GetOrCacheImpl(EtsCoroutine *coro, T number)
303 {
304 // NOTE(ipetrov): hack for 128 bit ObjectHeader
305 #if defined(PANDA_TARGET_64) && !defined(PANDA_USE_32_BIT_POINTER)
306 UNUSED_VAR(coro);
307 return {ToString(number), ToStringResult::STORE_NEW};
308 #else
309 EVENT_ETS_CACHE("Fastpath: create string from number with cache");
310 auto index = GetIndex(number);
311 // Atomic with acquire order reason: write of `elem` to array is dependency-ordered before reads from loaded `elem`
312 auto *elem = Base::Get(index, std::memory_order_acquire);
313 if (UNLIKELY(elem == nullptr)) {
314 [[maybe_unused]] EtsHandleScope scope(coro);
315 EtsHandle<EtsString> string(coro, ToString(number));
316 ASSERT(string.GetPtr() != nullptr);
317 // may trigger GC
318 StoreToCache(coro, string, number, index);
319 ASSERT(string.GetPtr() != nullptr);
320 return {string.GetPtr(), ToStringResult::STORE_NEW};
321 }
322 auto cached = elem->LoadData(); // r1
323 if (UNLIKELY(Elem::IsLocked(cached.flag))) {
324 return {ToString(number), ToStringResult::LOAD_FAIL_LOCKED};
325 }
326 auto cachedNumber = elem->LoadNumber(); // r2
327 if (LIKELY(cachedNumber == number)) {
328 if (LIKELY(elem->IsFresh(cached.flag))) { // r3
329 return {cached.string, ToStringResult::LOAD_CACHED};
330 }
331 return {ToString(number), ToStringResult::LOAD_FAIL_UPDATED};
332 }
333 return FinishUpdate(coro, number, elem, bit_cast<uint64_t>(cached));
334 #endif
335 }
336
337 template <typename T, typename Derived, typename Hash>
CacheAndGetNoCheck(EtsCoroutine * coro,T number,ObjectHeader * elem,uint64_t cached)338 EtsString *EtsToStringCache<T, Derived, Hash>::CacheAndGetNoCheck(EtsCoroutine *coro, T number, ObjectHeader *elem,
339 uint64_t cached)
340 {
341 ASSERT(elem != nullptr);
342 EVENT_ETS_CACHE("Fastpath: create string from number with cache");
343 return FinishUpdate(coro, number, EtsToStringCacheElement<T>::FromCoreType(elem), cached).first;
344 }
345
346 /* static */
347 template <typename T, typename Derived, typename Hash>
GetNoCache(T number)348 EtsString *EtsToStringCache<T, Derived, Hash>::GetNoCache(T number)
349 {
350 EVENT_ETS_CACHE("Slowpath: create string from number without cache");
351 return ToString(number);
352 }
353
354 /* static */
355 template <typename T, typename Derived, typename Hash>
Create(EtsCoroutine * coro)356 Derived *EtsToStringCache<T, Derived, Hash>::Create(EtsCoroutine *coro)
357 {
358 auto *etsClass = detail::EtsToStringCacheElement<T>::GetClass(coro);
359 if (etsClass == nullptr) {
360 return nullptr;
361 }
362 return static_cast<Derived *>(Base::Create(etsClass, SIZE, SpaceType::SPACE_TYPE_NON_MOVABLE_OBJECT));
363 }
364
365 template <typename T, typename Derived, typename Hash>
StoreToCache(EtsCoroutine * coro,EtsHandle<EtsString> & stringHandle,T number,uint32_t index)366 void EtsToStringCache<T, Derived, Hash>::StoreToCache(EtsCoroutine *coro, EtsHandle<EtsString> &stringHandle, T number,
367 uint32_t index)
368 {
369 auto *arrayClass = Base::GetCoreType()->template ClassAddr<Class>();
370 auto *elemClass = arrayClass->GetComponentType();
371 ASSERT(elemClass->GetObjectSize() == EtsToStringCacheElement<T>::GetUnalignedSize());
372 auto *elem = EtsToStringCacheElement<T>::Create(coro, stringHandle, number, EtsClass::FromRuntimeClass(elemClass));
373 // Atomic with release order reason: writes to elem should be visible in other threads
374 Base::Set(index, elem, std::memory_order_release);
375 }
376
377 template class EtsToStringCache<EtsDouble, DoubleToStringCache>;
378 template class EtsToStringCache<EtsFloat, FloatToStringCache>;
379 template class EtsToStringCache<EtsLong, LongToStringCache>;
380
381 } // namespace ark::ets::detail
382