Added String Interning Support to Dali. 60/248560/5
authorSubhransu Mohanty <sub.mohanty@samsung.com>
Wed, 25 Nov 2020 09:16:42 +0000 (18:16 +0900)
committerSubhransu Mohanty <sub.mohanty@samsung.com>
Tue, 1 Dec 2020 04:24:43 +0000 (13:24 +0900)
- thread safe StringPool.
- uses arena allocator to allocate the StringEntry object.
- Internal strings are allocated in String pages instead of indivisual heap allocation.
- Both StringData and length are stored in same location(StringEntry).
- StringPool implements FlatHash with open addresssing and Quadratic Probing (adopted from llvm)

Change-Id: Ib1b1d3bc74718dd613e924236bb1845e83f42353

automated-tests/src/dali-internal/CMakeLists.txt
automated-tests/src/dali-internal/utc-Dali-Internal-ConstString.cpp [new file with mode: 0644]
dali/internal/common/const-string.cpp [new file with mode: 0644]
dali/internal/common/const-string.h [new file with mode: 0644]
dali/internal/file.list

index 3ace2e3..e7d2310 100644 (file)
@@ -20,6 +20,7 @@ SET(TC_SOURCES
         utc-Dali-Internal-RotationGesture.cpp
         utc-Dali-Internal-TapGesture.cpp
         utc-Dali-Internal-TapGestureProcessor.cpp
+        utc-Dali-Internal-ConstString.cpp
 )
 
 LIST(APPEND TC_SOURCES
diff --git a/automated-tests/src/dali-internal/utc-Dali-Internal-ConstString.cpp b/automated-tests/src/dali-internal/utc-Dali-Internal-ConstString.cpp
new file mode 100644 (file)
index 0000000..02e65d1
--- /dev/null
@@ -0,0 +1,162 @@
+/*
+ * Copyright (c) 2020 Samsung Electronics Co., Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+#include <dali-test-suite-utils.h>
+#include <dali/public-api/dali-core.h>
+
+#include <random>
+#include <string>
+
+// Internal headers are allowed here
+#include <dali/internal/common/const-string.h>
+
+using namespace Dali;
+
+namespace
+{
+std::string RandomString(size_t length)
+{
+  static auto& chrs =
+    "0123456789"
+    "abcdefghijklmnopqrstuvwxyz"
+    "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
+
+  thread_local static std::mt19937                                          rg{std::random_device{}()};
+  thread_local static std::uniform_int_distribution<std::string::size_type> pick(0, sizeof(chrs) - 2);
+
+  std::string s;
+
+  s.reserve(length);
+
+  while(length--)
+    s += chrs[pick(rg)];
+
+  return s;
+}
+} // namespace
+
+void utc_dali_internal_conststring_startup(void)
+{
+  test_return_value = TET_UNDEF;
+}
+
+void utc_dali_internal_conststring_cleanup(void)
+{
+  test_return_value = TET_PASS;
+}
+
+int UtcDaliConstStringEmpty(void)
+{
+  Internal::ConstString str1;
+  DALI_TEST_EQUALS(str1.IsEmpty(), true, TEST_LOCATION);
+
+  Internal::ConstString str2("hello");
+  DALI_TEST_EQUALS(str2.IsEmpty(), false, TEST_LOCATION);
+
+  str2.Clear();
+  DALI_TEST_EQUALS(str2.IsEmpty(), true, TEST_LOCATION);
+
+  END_TEST;
+}
+
+int UtcDaliConstStringConstruct(void)
+{
+  Internal::ConstString str1("string1");
+  DALI_TEST_EQUALS(str1.GetCString(), "string1", TEST_LOCATION);
+
+  Internal::ConstString str2("string1");
+
+  DALI_TEST_EQUALS(str2.GetCString(), "string1", TEST_LOCATION);
+
+  bool samePointer = (str1.GetCString() == str2.GetCString());
+  DALI_TEST_EQUALS(samePointer, true, TEST_LOCATION);
+
+  str1.Clear();
+  DALI_TEST_EQUALS(str2.GetCString(), "string1", TEST_LOCATION);
+
+  END_TEST;
+}
+
+int UtcDaliConstStringConstructStringView(void)
+{
+  Internal::ConstString str1(std::string_view("random string"));
+
+  DALI_TEST_EQUALS(str1.GetStringView().data(), "random string", TEST_LOCATION);
+  DALI_TEST_EQUALS(str1.GetStringView().size(), 13, TEST_LOCATION);
+
+  END_TEST;
+}
+
+int UtcDaliConstStringSetString(void)
+{
+  Internal::ConstString str1(std::string_view("current string"));
+
+  DALI_TEST_EQUALS(str1.GetStringView().data(), "current string", TEST_LOCATION);
+
+  str1.SetString("new string");
+
+  DALI_TEST_EQUALS(str1.GetCString(), "new string", TEST_LOCATION);
+
+  END_TEST;
+}
+
+int UtcDaliConstStringStressTest(void)
+{
+  static constexpr size_t DB_SIZE = 2000;
+
+  std::vector<std::string> Database;
+  Database.reserve(DB_SIZE);
+
+  std::vector<Internal::ConstString> constStringDB;
+  constStringDB.reserve(DB_SIZE);
+
+  std::vector<Internal::ConstString> constStringDB1;
+  constStringDB1.reserve(DB_SIZE);
+
+  for(auto i = 0u; i < DB_SIZE; i++)
+  {
+    if(i % 3 == 0)
+    {
+      Database.push_back(RandomString(10));
+    }
+    else if(i % 4 == 0)
+    {
+      Database.push_back(RandomString(7));
+    }
+    else
+    {
+      Database.push_back(RandomString(11));
+    }
+    constStringDB.push_back(Internal::ConstString(Database[i]));
+    constStringDB1.push_back(Internal::ConstString(Database[i]));
+  }
+
+  // check eqality betwwen original string and constString
+  for(auto i = 0u; i < DB_SIZE; i++)
+  {
+    DALI_TEST_EQUALS(constStringDB[i].GetCString(), Database[i].c_str(), TEST_LOCATION);
+  }
+
+  // check pointer eqality betwwen 2 constString
+  for(auto i = 0u; i < DB_SIZE; i++)
+  {
+    bool pointerEqual = (constStringDB[i] == constStringDB1[i]);
+    DALI_TEST_EQUALS(pointerEqual, true, TEST_LOCATION);
+  }
+
+  END_TEST;
+}
diff --git a/dali/internal/common/const-string.cpp b/dali/internal/common/const-string.cpp
new file mode 100644 (file)
index 0000000..486fcf9
--- /dev/null
@@ -0,0 +1,323 @@
+/*
+ * Copyright (c) 2020 Samsung Electronics Co., Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+// CLASS HEADER
+#include <dali/internal/common/const-string.h>
+
+// EXTERNAL INCLUDES
+#include <cstddef>
+#include <cstring>
+#include <functional>
+#include <vector>
+#include <mutex>
+
+// local namespace
+namespace
+{
+class ArenaAllocator
+{
+
+public:
+  ArenaAllocator()
+  {
+    mPages.reserve(10);
+    EnsureSpace();
+  }
+  ~ArenaAllocator()
+  {
+    for(auto& page : mPages)
+    {
+      delete[] page;
+    }
+  }
+  char* Allocate(uint32_t size, uint32_t alignment)
+  {
+    uintptr_t mask          = alignment - 1;
+    uintptr_t alignedOffset = (~reinterpret_cast<uintptr_t>(mCursor) + 1) & mask;
+    uintptr_t totalSize     = size + alignedOffset;
+
+    if(totalSize > static_cast<uintptr_t>(mEnd - mCursor))
+    {
+      EnsureSpace();
+      alignedOffset = (~reinterpret_cast<uintptr_t>(mCursor) + 1) & mask;
+    }
+    char* ptr = mCursor + alignedOffset;
+    mCursor   = ptr + size;
+    return ptr;
+  }
+
+private:
+  void EnsureSpace()
+  {
+    constexpr size_t pageSize = 4096;
+    char*            page     = new char[pageSize];
+    mPages.push_back(page);
+    mCursor = page;
+    mEnd    = mCursor + pageSize;
+  }
+
+  char*              mCursor{nullptr};
+  char*              mEnd{nullptr};
+  std::vector<char*> mPages;
+};
+
+class StringEntry
+{
+public:
+  explicit StringEntry(uint16_t stringLength)
+  : mStringLength(stringLength)
+  {
+  }
+  size_t getStringLength() const
+  {
+    return mStringLength;
+  }
+  const char* GetStringData() const
+  {
+    return reinterpret_cast<const char*>(this + 1);
+  }
+
+  std::string_view GetString() const
+  {
+    return std::string_view(GetStringData(), getStringLength());
+  }
+  /**
+   * GetStringEntryFromData - Given key data that is known to be embedded
+   * into a StringEntry, return the StringEntry itself.
+   */
+  static StringEntry& GetStringEntryFromData(const char* stringData)
+  {
+    char* ptr = const_cast<char*>(stringData) - sizeof(StringEntry);
+    return *reinterpret_cast<StringEntry*>(ptr);
+  }
+  /**
+   * Create a StringEntry from string_view
+   *
+   */
+  static StringEntry* Create(std::string_view str, ArenaAllocator& allocator)
+  {
+    size_t length = str.size();
+
+    // Allocate a new item with space for the string at the end and a null
+    // terminator.
+    size_t allocSize = sizeof(StringEntry) + length + 1;
+    size_t alignment = alignof(StringEntry);
+
+    // Construct the value.
+    StringEntry* newItem = new(allocator.Allocate(allocSize, alignment)) StringEntry(length);
+
+    // Copy the string information.
+    char* strBuffer = const_cast<char*>(newItem->GetStringData());
+    if(length > 0)
+      memcpy(strBuffer, str.data(), length);
+    strBuffer[length] = 0; // Null terminate.
+    return newItem;
+  }
+
+private:
+  uint16_t mStringLength;
+};
+
+class StringPool
+{
+public:
+  unsigned Size() const
+  {
+    return mItems;
+  }
+  StringPool()
+  {
+    Init();
+  }
+
+  static StringPool& Instance()
+  {
+    static StringPool object;
+    return object;
+  }
+
+  const char* Intern(std::string_view str)
+  {
+    const std::lock_guard<std::mutex> lock(mMutex);
+
+    auto  bucketNumber = FindBucket(str);
+    auto& bucket   = mTable[bucketNumber];
+
+    if(bucket)
+    {
+      // string already exists
+      return bucket->GetStringData();
+    }
+
+    // assign the newly created StringEntry to the designated bucket.
+    bucket = StringEntry::Create(str, mAllocator);
+
+    ++mItems;
+
+    bucketNumber = RehashTable(bucketNumber);
+
+    return mTable[bucketNumber]->GetStringData();
+  }
+
+private:
+  unsigned NextBucket(unsigned currentBucket, unsigned totalBucket, unsigned ProbeAmount)
+  {
+    return (currentBucket + ProbeAmount) & (totalBucket - 1);
+  }
+
+  void Init()
+  {
+    unsigned newBuckets = 512;
+    /**
+     * for memory efficiency and cache locality we store the StringEntry and corresponding HashValue  in separate
+     * segments in this way each entry in the bucket will take 12byte instead of 16 if we keep them together.
+     * So we allocate one memory and treat 1st segment as a array of StringEntry* and the second segment as array of
+     * unsigned*.
+     */
+    mTable              = static_cast<StringEntry**>(calloc(newBuckets + 1, sizeof(StringEntry**) + sizeof(unsigned)));
+    mBuckets            = newBuckets;
+  }
+
+  unsigned FindBucket(std::string_view name)
+  {
+    unsigned  bucketSize    = mBuckets;
+    unsigned  fullHashValue = std::hash<std::string_view>{}(name);
+    unsigned  bucketNumber      = fullHashValue & (bucketSize - 1);
+    // point to the start of the hashvalue segment.
+    unsigned* hashTable     = reinterpret_cast<unsigned*>(mTable + mBuckets + 1);
+
+    unsigned probeAmt = 1;
+    while(true)
+    {
+      StringEntry* bucketItem = mTable[bucketNumber];
+      // If we found an empty bucket, this key isn't in the table yet, return it.
+      if(!bucketItem)
+      {
+        hashTable[bucketNumber] = fullHashValue;
+        return bucketNumber;
+      }
+
+      if(hashTable[bucketNumber] == fullHashValue)
+      {
+        // If the full hash value matches, check deeply for a match.
+        if(name == bucketItem->GetString())
+        {
+          // We found a match!
+          return bucketNumber;
+        }
+      }
+
+      // Okay, we didn't find the item.  Probe to the next bucket.
+      bucketNumber = NextBucket(bucketNumber, bucketSize, probeAmt);
+
+      // Use quadratic probing, it has fewer clumping artifacts than linear
+      ++probeAmt;
+    }
+  }
+  unsigned RehashTable(unsigned bucketNumber)
+  {
+    unsigned  newSize;
+    unsigned* hashTable = reinterpret_cast<unsigned*>(mTable + mBuckets + 1);
+
+    // If the hash table is now more than 3/4 full.
+    // grow/rehash the table.
+    if((mItems * 4) > (mBuckets * 3))
+    {
+      newSize = mBuckets * 2;
+    }
+    else
+    {
+      return bucketNumber;
+    }
+    unsigned newBucketNumber = bucketNumber;
+    // Allocate one extra bucket which will always be non-empty.
+    auto newTable = static_cast<StringEntry**>(calloc(newSize + 1, sizeof(StringEntry*) + sizeof(unsigned)));
+    // point to the start of the hashvalue segment. as the pointer is of type StringEntry* , but the
+    // second segment keeps only unsigned data hence the reinterpret_cast.
+    unsigned* newHashTable = reinterpret_cast<unsigned*>(newTable + newSize + 1);
+
+    // Rehash all the items into their new buckets.
+    for(unsigned i = 0, j = mBuckets; i != j; ++i)
+    {
+      StringEntry* bucket = mTable[i];
+      if(bucket)
+      {
+        // Fast case, bucket available.
+        unsigned fullHash  = hashTable[i];
+        unsigned newBucket = fullHash & (newSize - 1);
+        if(!newTable[newBucket])
+        {
+          newTable[fullHash & (newSize - 1)]     = bucket;
+          newHashTable[fullHash & (newSize - 1)] = fullHash;
+          if(i == bucketNumber)
+            newBucketNumber = newBucket;
+          continue;
+        }
+
+        // Otherwise probe for a spot.
+        unsigned ProbeSize = 1;
+        do
+        {
+          newBucket = NextBucket(newBucket, newSize, ProbeSize++);
+        } while(newTable[newBucket]);
+
+        // Finally found a slot.  Fill it in.
+        newTable[newBucket]     = bucket;
+        newHashTable[newBucket] = fullHash;
+        if(i == bucketNumber)
+          newBucketNumber = newBucket;
+      }
+    }
+
+    free(mTable);
+
+    mTable   = newTable;
+    mBuckets = newSize;
+    return newBucketNumber;
+  }
+
+private:
+  std::mutex     mMutex;
+  ArenaAllocator mAllocator;
+  StringEntry**  mTable{nullptr};
+  unsigned       mBuckets{0};
+  unsigned       mItems{0};
+};
+
+} // namespace
+
+Dali::Internal::ConstString::ConstString(std::string_view str)
+{
+  mString = StringPool::Instance().Intern(str);
+}
+
+size_t Dali::Internal::ConstString::GetLength() const
+{
+  return mString ? StringEntry::GetStringEntryFromData(mString).getStringLength() : 0;
+}
+
+void Dali::Internal::ConstString::SetString(std::string_view str)
+{
+  if(str.empty())
+  {
+    mString = nullptr;
+  }
+  else
+  {
+    mString = StringPool::Instance().Intern(str);
+  }
+}
\ No newline at end of file
diff --git a/dali/internal/common/const-string.h b/dali/internal/common/const-string.h
new file mode 100644 (file)
index 0000000..49b116c
--- /dev/null
@@ -0,0 +1,238 @@
+#ifndef DALI_INTERNAL_CONST_STRING_H
+#define DALI_INTERNAL_CONST_STRING_H
+
+/*
+ * Copyright (c) 2020 Samsung Electronics Co., Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+#include <string_view>
+
+namespace Dali
+{
+namespace Internal
+{
+/**
+ * A uniqued constant string class.
+ *
+ * Provides an efficient way to store strings as uniqued strings. After the
+ * strings are uniqued, finding strings that are equal to one another is very
+ * fast as just the pointers need to be compared. It also allows for many
+ * common strings from many different sources to be shared to keep the memory
+ * footprint low.
+ *
+ * No reference counting is done on strings that are added to the string
+ * pool, once strings are added they are in the string pool for the life of
+ * the program.
+ */
+class ConstString
+{
+public:
+  /**
+   * Default constructor
+   *
+   * Initializes the string to an empty string.
+   */
+  ConstString() = default;
+
+  explicit ConstString(std::string_view str);
+
+  /**
+   * Convert to bool operator.
+   *
+   * This allows code to check a ConstString object to see if it contains a
+   * valid string using code such as:
+   *
+   * \code
+   * ConstString str(...);
+   * if (str)
+   * { ...
+   * \endcode
+   *
+   * \return
+   *     /b True this object contains a valid non-empty C string, \b
+   *     false otherwise.
+   */
+  explicit operator bool() const
+  {
+    return !IsEmpty();
+  }
+
+  /**
+   * Equal to operator
+   *
+   * Returns true if this string is equal to the string in \a rhs. This
+   * operation is very fast as it results in a pointer comparison since all
+   * strings are in a uniqued in a global string pool.
+   *
+   * \param[in] rhs
+   *     Another string object to compare this object to.
+   *
+   * \return
+   *     true if this object is equal to \a rhs.
+   *     false if this object is not equal to \a rhs.
+   */
+  bool operator==(ConstString rhs) const
+  {
+    // We can do a pointer compare to compare these strings since they must
+    // come from the same pool in order to be equal.
+    return mString == rhs.mString;
+  }
+
+  /**
+   * Equal to operator against a non-ConstString value.
+   *
+   * Returns true if this string is equal to the string in \a rhs.
+   *
+   * \param[in] rhs
+   *     Another string object to compare this object to.
+   *
+   * \return
+   *     \b true if this object is equal to \a rhs.
+   *     \b false if this object is not equal to \a rhs.
+   */
+  bool operator==(const char* rhs) const
+  {
+    // ConstString differentiates between empty strings and nullptr strings, but
+    // StringRef doesn't. Therefore we have to do this check manually now.
+    if(mString == nullptr && rhs != nullptr)
+      return false;
+    if(mString != nullptr && rhs == nullptr)
+      return false;
+
+    return GetStringView() == rhs;
+  }
+
+  /**
+   * Not equal to operator
+   *
+   * Returns true if this string is not equal to the string in \a rhs. This
+   * operation is very fast as it results in a pointer comparison since all
+   * strings are in a uniqued in a global string pool.
+   *
+   * \param[in] rhs
+   *     Another string object to compare this object to.
+   *
+   * \return
+   *     \b true if this object is not equal to \a rhs.
+   *     \b false if this object is equal to \a rhs.
+   */
+  bool operator!=(ConstString rhs) const
+  {
+    return mString != rhs.mString;
+  }
+
+  /**
+   * Not equal to operator against a non-ConstString value.
+   *
+   * Returns true if this string is not equal to the string in \a rhs.
+   *
+   * \param[in] rhs
+   *     Another string object to compare this object to.
+   *
+   * \return \b true if this object is not equal to \a rhs, false otherwise.
+   */
+  bool operator!=(const char* rhs) const
+  {
+    return !(*this == rhs);
+  }
+
+  /**
+   * Get the string value as a std::string_view
+   *
+   * \return
+   *     Returns a new std::string_view object filled in with the
+   *     needed data.
+   */
+  std::string_view GetStringView() const
+  {
+    return mString ? std::string_view(mString, GetLength()) : std::string_view();
+  }
+
+  /**
+   * Get the string value as a C string.
+   *
+   * Get the value of the contained string as a NULL terminated C string
+   * value. This function will always return nullptr if the string is not valid.
+   *  So this function is a direct accessor to the string pointer value.
+   *
+   * \return
+   *     Returns nullptr the string is invalid, otherwise the C string
+   *     value contained in this object.
+   */
+  const char* GetCString() const
+  {
+    return mString;
+  }
+
+  /**
+   * Get the length in bytes of string value.
+   *
+   * The string pool stores the length of the string, so we can avoid calling
+   * strlen() on the pointer value with this function.
+   *
+   * \return
+   *     Returns the number of bytes that this string occupies in
+   *     memory, not including the NULL termination byte.
+   */
+  size_t GetLength() const;
+
+  /**
+   * Clear this object's state.
+   *
+   * Clear any contained string and reset the value to the empty string
+   * value.
+   */
+  void Clear()
+  {
+    mString = nullptr;
+  }
+
+  /**
+   * Test for empty string.
+   *
+   * \return
+   *     \b true if the contained string is empty.
+   *     \b false if the contained string is not empty.
+   */
+  bool IsEmpty() const
+  {
+    return mString == nullptr || mString[0] == '\0';
+  }
+
+  /**
+   * Set the string_view value.
+   *
+   * Set the string value in the object by uniquing the \a str string value
+   * in our global string pool.
+   *
+   * If the string is already exists in the global string pool, it finds the
+   * current entry and returns the existing value. If it doesn't exist, it is
+   * added to the string pool.
+   *
+   * \param[in] str
+   *     A string_view to add to the string pool.
+   */
+  void SetString(std::string_view str);
+
+private:
+  const char* mString{nullptr};
+};
+
+} // namespace Internal
+
+} // namespace Dali
+
+#endif // DALI_INTERNAL_CONST_STRING_H
index e3f3e1e..07cf4ff 100644 (file)
@@ -11,6 +11,8 @@ SET( internal_src_files
   ${internal_src_dir}/common/image-sampler.cpp
   ${internal_src_dir}/common/image-attributes.cpp
   ${internal_src_dir}/common/fixed-size-memory-pool.cpp
+  ${internal_src_dir}/common/const-string.cpp
+
 
   ${internal_src_dir}/event/actors/actor-impl.cpp
   ${internal_src_dir}/event/actors/actor-property-handler.cpp