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