SfmtTests.cpp 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278
  1. /*
  2. * Copyright (c) Contributors to the Open 3D Engine Project.
  3. * For complete copyright and license terms please see the LICENSE at the root of this distribution.
  4. *
  5. * SPDX-License-Identifier: Apache-2.0 OR MIT
  6. *
  7. */
  8. #include <AzCore/Math/Sfmt.h>
  9. #include <AzCore/UnitTest/TestTypes.h>
  10. #include <AzCore/Math/Vector4.h>
  11. #include <AzCore/std/sort.h>
  12. #include <AzCore/std/parallel/semaphore.h>
  13. using namespace AZ;
  14. namespace UnitTest
  15. {
  16. class MATH_SfmtTest
  17. : public LeakDetectionFixture
  18. {
  19. static const int BLOCK_SIZE = 100000;
  20. static const int BLOCK_SIZE64 = 50000;
  21. static const int COUNT = 1000;
  22. AZ::u64* array1;
  23. AZ::u64* array2;
  24. public:
  25. void SetUp() override
  26. {
  27. LeakDetectionFixture::SetUp();
  28. array1 = (AZ::u64*)azmalloc(sizeof(AZ::u64) * 2 * (BLOCK_SIZE / 4), AZStd::alignment_of<AZ::Vector4>::value);
  29. array2 = (AZ::u64*)azmalloc(sizeof(AZ::u64) * 2 * (10000 / 4), AZStd::alignment_of<AZ::Vector4>::value);
  30. }
  31. void TearDown() override
  32. {
  33. azfree(array1);
  34. azfree(array2);
  35. LeakDetectionFixture::TearDown();
  36. }
  37. void check32()
  38. {
  39. int i;
  40. AZ::u32* array32 = (AZ::u32*)array1;
  41. AZ::u32* array32_2 = (AZ::u32*)array2;
  42. AZ::u32 ini[] = { 0x1234, 0x5678, 0x9abc, 0xdef0 };
  43. Sfmt g;
  44. EXPECT_LT(g.GetMinArray32Size(), 10000);
  45. g.FillArray32(array32, 10000);
  46. g.FillArray32(array32_2, 10000);
  47. g.Seed();
  48. for (i = 0; i < 10000; i++)
  49. {
  50. EXPECT_NE(array32[i], g.Rand32());
  51. }
  52. for (i = 0; i < 700; i++)
  53. {
  54. EXPECT_NE(array32_2[i], g.Rand32());
  55. }
  56. g.Seed(ini, 4);
  57. g.FillArray32(array32, 10000);
  58. g.FillArray32(array32_2, 10000);
  59. g.Seed(ini, 4);
  60. for (i = 0; i < 10000; i++)
  61. {
  62. EXPECT_EQ(array32[i], g.Rand32());
  63. }
  64. for (i = 0; i < 700; i++)
  65. {
  66. EXPECT_EQ(array32_2[i], g.Rand32());
  67. }
  68. }
  69. void check64()
  70. {
  71. int i;
  72. AZ::u64* array64 = (AZ::u64*)array1;
  73. AZ::u64* array64_2 = (AZ::u64*)array2;
  74. AZ::u32 ini[] = { 5, 4, 3, 2, 1 };
  75. Sfmt g;
  76. EXPECT_LT(g.GetMinArray64Size(), 5000);
  77. g.FillArray64(array64, 5000);
  78. g.FillArray64(array64_2, 5000);
  79. g.Seed();
  80. for (i = 0; i < 5000; i++)
  81. {
  82. EXPECT_NE(array64[i], g.Rand64());
  83. }
  84. for (i = 0; i < 700; i++)
  85. {
  86. EXPECT_NE(array64_2[i], g.Rand64());
  87. }
  88. g.Seed(ini, 5);
  89. g.FillArray64(array64, 5000);
  90. g.FillArray64(array64_2, 5000);
  91. g.Seed(ini, 5);
  92. for (i = 0; i < 5000; i++)
  93. {
  94. EXPECT_EQ(array64[i], g.Rand64());
  95. }
  96. for (i = 0; i < 700; i++)
  97. {
  98. EXPECT_EQ(array64_2[i], g.Rand64());
  99. }
  100. }
  101. template<size_t NumThreads, size_t NumResultsPerThread, typename ResultType>
  102. bool ParallelTest()
  103. {
  104. Sfmt sfmt;
  105. // Arbitrary but deterministic seed values to guarantee that we get the same random numbers produced every time.
  106. constexpr AZ::u32 buffer[32] = {
  107. 0x8236ed73, 0x854aa369, 0xc7b68864, 0x5f1e49da, 0x58ea5a08, 0xa33fc74b, 0x0336bd81, 0x8d3c11ac,
  108. 0x7312bc57, 0x92fee3d1, 0x4b852f9f, 0xa7ac02ad, 0x4d72fb7a, 0x630641c6, 0x1edbeebf, 0xc18fdabe,
  109. 0xc6588dba, 0x6a821cf5, 0xec7e24b3, 0x44246d7e, 0x24af2f32, 0x4f64d44c, 0x44b24116, 0x65585572,
  110. 0x0f95038d, 0xd3e4c521, 0x6eea6bc1, 0x6d651ab5, 0x25dfc39e, 0x7502d183, 0xd32fc9bf, 0x9854093f };
  111. // Seed the sfmt generator.
  112. sfmt.Seed(buffer, AZ_ARRAY_SIZE(buffer));
  113. // Create a single array that will hold all the results, but we'll partition it so that each thread will write
  114. // its results to a different subset.
  115. AZStd::array<ResultType, NumResultsPerThread * NumThreads> results;
  116. AZStd::thread threads[NumThreads];
  117. // Use a semaphore to block thread execution until all the threads are created so that we can synchronize
  118. // their start times to the best of our ability.
  119. AZStd::semaphore startSync;
  120. // Spawn a set of threads that will each loop through and call the RandMethod (Rand32 or Rand64) for
  121. // a given number of times.
  122. for (size_t threadIdx = 0; threadIdx < AZ_ARRAY_SIZE(threads); ++threadIdx)
  123. {
  124. const size_t startOffset = NumResultsPerThread * threadIdx;
  125. auto threadFunc = [&startSync, startOffset, &results, &sfmt]()
  126. {
  127. // Block so that we can start all the threads at the same time after creation.
  128. startSync.acquire();
  129. for (int i = 0; i < NumResultsPerThread; ++i)
  130. {
  131. // Call Rand32 when uint32_t is the passed-in type, and Rand64 if uint64_t is the passed-in type.
  132. if constexpr (AZStd::is_same_v<ResultType, uint32_t>)
  133. {
  134. results[startOffset + i] = sfmt.Rand32();
  135. }
  136. else
  137. {
  138. results[startOffset + i] = sfmt.Rand64();
  139. }
  140. }
  141. };
  142. threads[threadIdx] = AZStd::thread(threadFunc);
  143. }
  144. // Now that all the threads are created, signal them to start.
  145. startSync.release(NumThreads);
  146. // Wait for all the threads to complete.
  147. for (auto& thread : threads)
  148. {
  149. thread.join();
  150. }
  151. // Sort the results from all the threads so that we can easily detect duplicates.
  152. AZStd::sort(results.begin(), results.end());
  153. // Look for duplicates and stop if we find one, since that's a full test failure. There's no need to keep iterating.
  154. for (size_t idx = 1; idx < results.size(); idx++)
  155. {
  156. if (results[idx - 1] == results[idx])
  157. {
  158. EXPECT_NE(results[idx - 1], results[idx]);
  159. return false;
  160. }
  161. }
  162. // Test was successful
  163. return true;
  164. }
  165. };
  166. TEST_F(MATH_SfmtTest, Test32Bit)
  167. {
  168. check32();
  169. }
  170. TEST_F(MATH_SfmtTest, Test64Bit)
  171. {
  172. check64();
  173. }
  174. TEST_F(MATH_SfmtTest, TestParallel32)
  175. {
  176. // This test verifies that we can call Rand32() successfully in parallel.
  177. // It also performs a reasonable verification that we haven't reintroduced a race condition in which the random number
  178. // sequence could get repeated for periods of time if multiple threads hit the point where Sfmt refills its number cache
  179. // simultaneously. Because it was a race condition, this test isn't foolproof, but the test conditions set below were
  180. // causing a near-100% failure rate with the previous race condition and has a 100% success rate with the fix.
  181. // The following numbers were chosen to be high enough to expose the problem at least once per run but low enough to keep
  182. // the overall test runtime down.
  183. constexpr size_t NumIterations = 5;
  184. constexpr size_t NumThreads = 8;
  185. constexpr size_t NumResultsPerThread = 2000;
  186. // Run the test single-threaded once to verify that we don't have any numerical collisions in our test set and test size.
  187. bool testSetHasNoNumericalCollisions = ParallelTest<1, NumThreads * NumResultsPerThread, uint32_t>();
  188. ASSERT_TRUE(testSetHasNoNumericalCollisions);
  189. // Now run multi-threaded across multiple iterations to verify that we don't hit the race condition.
  190. for (size_t iterations = 0; iterations < NumIterations; iterations++)
  191. {
  192. bool parallelHasNoNumericalCollisions = ParallelTest<NumThreads, NumResultsPerThread, uint32_t>();
  193. ASSERT_TRUE(parallelHasNoNumericalCollisions);
  194. }
  195. }
  196. TEST_F(MATH_SfmtTest, TestParallel64)
  197. {
  198. // This test verifies that we can call Rand64() successfully in parallel.
  199. // The following numbers were chosen to be high enough to expose the problem at least once per run but low enough to keep
  200. // the overall test runtime down.
  201. constexpr size_t NumIterations = 5;
  202. constexpr size_t NumThreads = 8;
  203. constexpr size_t NumResultsPerThread = 2000;
  204. // Run the test single-threaded once to verify that we don't have any numerical collisions in our test set and test size.
  205. bool testSetHasNoNumericalCollisions = ParallelTest<1, NumThreads * NumResultsPerThread, uint64_t>();
  206. ASSERT_TRUE(testSetHasNoNumericalCollisions);
  207. // Now run multi-threaded across multiple iterations to verify that we don't hit the race condition.
  208. for (size_t iterations = 0; iterations < NumIterations; iterations++)
  209. {
  210. bool parallelHasNoNumericalCollisions = ParallelTest<NumThreads, NumResultsPerThread, uint64_t>();
  211. ASSERT_TRUE(parallelHasNoNumericalCollisions);
  212. }
  213. }
  214. TEST_F(MATH_SfmtTest, TestParallelInterleaved)
  215. {
  216. Sfmt sfmt;
  217. auto threadFunc = [&sfmt]()
  218. {
  219. for (int i = 0; i < 10000; ++i)
  220. {
  221. AZ::u64 roll = sfmt.Rand64();
  222. if (roll % 2 == 0)
  223. {
  224. sfmt.Rand32();
  225. }
  226. else
  227. {
  228. sfmt.Rand64();
  229. }
  230. }
  231. };
  232. AZStd::thread threads[8];
  233. for (size_t threadIdx = 0; threadIdx < AZ_ARRAY_SIZE(threads); ++threadIdx)
  234. {
  235. threads[threadIdx] = AZStd::thread(threadFunc);
  236. }
  237. for (auto& thread : threads)
  238. {
  239. thread.join();
  240. }
  241. }
  242. }