• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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