From 8c0d7c1ebc5e9b2ffbb02170019f5b0c9bbf4262 Mon Sep 17 00:00:00 2001 From: Tarek Mahmoud Sayed Date: Wed, 14 Oct 2020 06:46:37 -0700 Subject: [PATCH] Optimize the Linguistic String Search Operations (#43065) --- .../System.Globalization.Native/pal_collation.c | 390 ++++++++++++++++----- .../pal_icushim_internal.h | 7 +- .../pal_icushim_internal_android.h | 2 + .../System.Runtime/tests/System/StringTests.cs | 59 ++++ 4 files changed, 369 insertions(+), 89 deletions(-) diff --git a/src/libraries/Native/Unix/System.Globalization.Native/pal_collation.c b/src/libraries/Native/Unix/System.Globalization.Native/pal_collation.c index 88334c9..e9468ed 100644 --- a/src/libraries/Native/Unix/System.Globalization.Native/pal_collation.c +++ b/src/libraries/Native/Unix/System.Globalization.Native/pal_collation.c @@ -38,8 +38,16 @@ c_static_assert_msg(USEARCH_DONE == -1, "managed side requires -1 for not found" // change ICU's default behavior here isn't really justified unless someone has a strong reason // for !StringSort to behave differently. +#define USED_STRING_SEARCH ((UStringSearch*) (-1)) + typedef struct { int32_t key; UCollator* UCollator; } TCollatorMap; +typedef struct SearchIteratorNode +{ + UStringSearch* searchIterator; + struct SearchIteratorNode* next; +} SearchIteratorNode; + /* * For increased performance, we cache the UCollator objects for a locale and * share them across threads. This is safe (and supported in ICU) if we ensure @@ -48,6 +56,7 @@ typedef struct { int32_t key; UCollator* UCollator; } TCollatorMap; struct SortHandle { UCollator* collatorsPerOption[CompareOptionsMask + 1]; + SearchIteratorNode searchIteratorList[CompareOptionsMask + 1]; }; typedef struct { UChar* items; size_t size; } UCharList; @@ -376,6 +385,29 @@ void GlobalizationNative_CloseSortHandle(SortHandle* pSortHandle) { if (pSortHandle->collatorsPerOption[i] != NULL) { + UStringSearch* pSearch = pSortHandle->searchIteratorList[i].searchIterator; + if (pSearch != NULL) + { + if (pSearch != USED_STRING_SEARCH) + { + usearch_close(pSearch); + } + pSortHandle->searchIteratorList[i].searchIterator = NULL; + SearchIteratorNode* pNext = pSortHandle->searchIteratorList[i].next; + pSortHandle->searchIteratorList[i].next = NULL; + + while (pNext != NULL) + { + if (pNext->searchIterator != NULL && pNext->searchIterator != USED_STRING_SEARCH) + { + usearch_close(pNext->searchIterator); + } + SearchIteratorNode* pCurrent = pNext; + pNext = pCurrent->next; + free(pCurrent); + } + } + ucol_close(pSortHandle->collatorsPerOption[i]); pSortHandle->collatorsPerOption[i] = NULL; } @@ -413,6 +445,174 @@ static const UCollator* GetCollatorFromSortHandle(SortHandle* pSortHandle, int32 } } +// CreateNewSearchNode will create a new node in the linked list and mark this node search handle as borrowed handle. +static inline int32_t CreateNewSearchNode(SortHandle* pSortHandle, int32_t options) +{ + SearchIteratorNode* node = (SearchIteratorNode*) malloc(sizeof(SearchIteratorNode)); + if (node == NULL) + { + return FALSE; + } + + node->searchIterator = USED_STRING_SEARCH; // Mark the new node search handle as borrowed. + node->next = NULL; + + SearchIteratorNode* pCurrent = &pSortHandle->searchIteratorList[options]; + assert(pCurrent->searchIterator != NULL && "Search iterator not expected to be NULL at this stage."); + + SearchIteratorNode* pNull = NULL; + do + { + if (pCurrent->next == NULL && pal_atomic_cas_ptr((void* volatile*)&(pCurrent->next), node, pNull)) + { + break; + } + + assert(pCurrent->next != NULL && "next pointer shouldn't be null."); + + pCurrent = pCurrent->next; + + } while (TRUE); + + return TRUE; +} + +// Restore previously borrowed search handle to the linked list. +static inline int32_t RestoreSearchHandle(SortHandle* pSortHandle, UStringSearch* pSearchIterator, int32_t options) +{ + SearchIteratorNode* pCurrent = &pSortHandle->searchIteratorList[options]; + + while (pCurrent != NULL) + { + if (pCurrent->searchIterator == USED_STRING_SEARCH && pal_atomic_cas_ptr((void* volatile*)&(pCurrent->searchIterator), pSearchIterator, USED_STRING_SEARCH)) + { + return TRUE; + } + pCurrent = pCurrent->next; + } + + return FALSE; +} + +// return -1 if couldn't borrow search handle from the SortHandle cache, otherwise, it return the slot number of the cache. +static const int32_t GetSearchIteratorUsingCollator( + SortHandle* pSortHandle, + const UCollator* pColl, + const UChar* lpTarget, + int32_t cwTargetLength, + const UChar* lpSource, + int32_t cwSourceLength, + int32_t options, + UStringSearch** pSearchIterator) +{ + options &= CompareOptionsMask; + *pSearchIterator = pSortHandle->searchIteratorList[options].searchIterator; + UErrorCode err = U_ZERO_ERROR; + + if (*pSearchIterator == NULL) + { + *pSearchIterator = usearch_openFromCollator(lpTarget, cwTargetLength, lpSource, cwSourceLength, pColl, NULL, &err); + if (!U_SUCCESS(err)) + { + assert(FALSE && "Couldn't open the search iterator."); + return -1; + } + + UStringSearch* pNull = NULL; + if (!pal_atomic_cas_ptr((void* volatile*)&(pSortHandle->searchIteratorList[options].searchIterator), USED_STRING_SEARCH, pNull)) + { + if (!CreateNewSearchNode(pSortHandle, options)) + { + usearch_close(*pSearchIterator); + return -1; + } + } + + return options; + } + + assert(*pSearchIterator != NULL && "Should having a valid search handle at this stage."); + + SearchIteratorNode* pCurrent = &pSortHandle->searchIteratorList[options]; + + while (*pSearchIterator == USED_STRING_SEARCH || !pal_atomic_cas_ptr((void* volatile*)&(pCurrent->searchIterator), USED_STRING_SEARCH, *pSearchIterator)) + { + pCurrent = pCurrent->next; + if (pCurrent == NULL) + { + *pSearchIterator = NULL; + break; + } + + *pSearchIterator = pCurrent->searchIterator; + } + + if (*pSearchIterator == NULL) // Couldn't find any available handle to borrow then create a new one. + { + *pSearchIterator = usearch_openFromCollator(lpTarget, cwTargetLength, lpSource, cwSourceLength, pColl, NULL, &err); + if (!U_SUCCESS(err)) + { + assert(FALSE && "Couldn't open a new search iterator."); + return -1; + } + + if (!CreateNewSearchNode(pSortHandle, options)) + { + usearch_close(*pSearchIterator); + return -1; + } + + return options; + } + + usearch_setText(*pSearchIterator, lpSource, cwSourceLength, &err); + if (!U_SUCCESS(err)) + { + int32_t r = RestoreSearchHandle(pSortHandle, *pSearchIterator, options); + assert(r && "restoring search handle shouldn't fail."); + return -1; + } + + usearch_setPattern(*pSearchIterator, lpTarget, cwTargetLength, &err); + if (!U_SUCCESS(err)) + { + int32_t r = RestoreSearchHandle(pSortHandle, *pSearchIterator, options); + assert(r && "restoring search handle shouldn't fail."); + return -1; + } + + return options; +} + +// return -1 if couldn't borrow search handle from the SortHandle cache, otherwise, it return the slot number of the cache. +static inline const int32_t GetSearchIterator( + SortHandle* pSortHandle, + const UChar* lpTarget, + int32_t cwTargetLength, + const UChar* lpSource, + int32_t cwSourceLength, + int32_t options, + UStringSearch** pSearchIterator) +{ + UErrorCode err = U_ZERO_ERROR; + const UCollator* pColl = GetCollatorFromSortHandle(pSortHandle, options, &err); + if (!U_SUCCESS(err)) + { + assert(FALSE && "Couldn't get the collator."); + return -1; + } + + return GetSearchIteratorUsingCollator( + pSortHandle, + pColl, + lpTarget, + cwTargetLength, + lpSource, + cwSourceLength, + options, + pSearchIterator); +} + int32_t GlobalizationNative_GetSortVersion(SortHandle* pSortHandle) { UErrorCode err = U_ZERO_ERROR; @@ -499,26 +699,25 @@ int32_t GlobalizationNative_IndexOf( } UErrorCode err = U_ZERO_ERROR; - const UCollator* pColl = GetCollatorFromSortHandle(pSortHandle, options, &err); - if (U_SUCCESS(err)) + UStringSearch* pSearch; + int32_t searchCacheSlot = GetSearchIterator(pSortHandle, lpTarget, cwTargetLength, lpSource, cwSourceLength, options, &pSearch); + if (searchCacheSlot < 0) { - UStringSearch* pSearch = usearch_openFromCollator(lpTarget, cwTargetLength, lpSource, cwSourceLength, pColl, NULL, &err); + return result; + } - if (U_SUCCESS(err)) - { - result = usearch_first(pSearch, &err); + result = usearch_first(pSearch, &err); - // if the search was successful, - // we'll try to get the matched string length. - if(result != USEARCH_DONE && pMatchedLength != NULL) - { - *pMatchedLength = usearch_getMatchedLength(pSearch); - } - usearch_close(pSearch); - } + // if the search was successful, + // we'll try to get the matched string length. + if (result != USEARCH_DONE && pMatchedLength != NULL) + { + *pMatchedLength = usearch_getMatchedLength(pSearch); } + RestoreSearchHandle(pSortHandle, pSearch, searchCacheSlot); + return result; } @@ -558,26 +757,25 @@ int32_t GlobalizationNative_LastIndexOf( } UErrorCode err = U_ZERO_ERROR; - const UCollator* pColl = GetCollatorFromSortHandle(pSortHandle, options, &err); + UStringSearch* pSearch; - if (U_SUCCESS(err)) + int32_t searchCacheSlot = GetSearchIterator(pSortHandle, lpTarget, cwTargetLength, lpSource, cwSourceLength, options, &pSearch); + if (searchCacheSlot < 0) { - UStringSearch* pSearch = usearch_openFromCollator(lpTarget, cwTargetLength, lpSource, cwSourceLength, pColl, NULL, &err); + return result; + } - if (U_SUCCESS(err)) - { - result = usearch_last(pSearch, &err); + result = usearch_last(pSearch, &err); - // if the search was successful, - // we'll try to get the matched string length. - if (result != USEARCH_DONE && pMatchedLength != NULL) - { - *pMatchedLength = usearch_getMatchedLength(pSearch); - } - usearch_close(pSearch); - } + // if the search was successful, + // we'll try to get the matched string length. + if (result != USEARCH_DONE && pMatchedLength != NULL) + { + *pMatchedLength = usearch_getMatchedLength(pSearch); } + RestoreSearchHandle(pSortHandle, pSearch, searchCacheSlot); + return result; } @@ -709,35 +907,45 @@ static int32_t SimpleAffix(const UCollator* pCollator, UErrorCode* pErrorCode, c return result; } -static int32_t ComplexStartsWith(const UCollator* pCollator, UErrorCode* pErrorCode, const UChar* pPattern, int32_t patternLength, const UChar* pText, int32_t textLength, int32_t* pMatchedLength) +static int32_t ComplexStartsWith(SortHandle* pSortHandle, const UChar* pPattern, int32_t patternLength, const UChar* pText, int32_t textLength, int32_t options, int32_t* pMatchedLength) { int32_t result = FALSE; + UErrorCode err = U_ZERO_ERROR; - UStringSearch* pSearch = usearch_openFromCollator(pPattern, patternLength, pText, textLength, pCollator, NULL, pErrorCode); - if (U_SUCCESS(*pErrorCode)) + const UCollator* pCollator = GetCollatorFromSortHandle(pSortHandle, options, &err); + if (!U_SUCCESS(err)) { - int32_t idx = usearch_first(pSearch, pErrorCode); - if (idx != USEARCH_DONE) - { - if (idx == 0) - { - result = TRUE; - } - else - { - result = CanIgnoreAllCollationElements(pCollator, pText, idx); - } + return result; + } - if (result && pMatchedLength != NULL) - { - // adjust matched length to account for all the elements we implicitly consumed at beginning of string - *pMatchedLength = idx + usearch_getMatchedLength(pSearch); - } + UStringSearch* pSearch; + int32_t searchCacheSlot = GetSearchIteratorUsingCollator(pSortHandle, pCollator, pPattern, patternLength, pText, textLength, options, &pSearch); + if (searchCacheSlot < 0) + { + return result; + } + + int32_t idx = usearch_first(pSearch, &err); + if (idx != USEARCH_DONE) + { + if (idx == 0) + { + result = TRUE; + } + else + { + result = CanIgnoreAllCollationElements(pCollator, pText, idx); } - usearch_close(pSearch); + if (result && pMatchedLength != NULL) + { + // adjust matched length to account for all the elements we implicitly consumed at beginning of string + *pMatchedLength = idx + usearch_getMatchedLength(pSearch); + } } + RestoreSearchHandle(pSortHandle, pSearch, searchCacheSlot); + return result; } @@ -753,57 +961,65 @@ int32_t GlobalizationNative_StartsWith( int32_t options, int32_t* pMatchedLength) { + if (options > CompareOptionsIgnoreCase) + { + return ComplexStartsWith(pSortHandle, lpTarget, cwTargetLength, lpSource, cwSourceLength, options, pMatchedLength); + } + UErrorCode err = U_ZERO_ERROR; const UCollator* pCollator = GetCollatorFromSortHandle(pSortHandle, options, &err); - if (!U_SUCCESS(err)) { return FALSE; } - else if (options > CompareOptionsIgnoreCase) - { - return ComplexStartsWith(pCollator, &err, lpTarget, cwTargetLength, lpSource, cwSourceLength, pMatchedLength); - } - else - { - return SimpleAffix(pCollator, &err, lpTarget, cwTargetLength, lpSource, cwSourceLength, TRUE, pMatchedLength); - } + + return SimpleAffix(pCollator, &err, lpTarget, cwTargetLength, lpSource, cwSourceLength, TRUE, pMatchedLength); } -static int32_t ComplexEndsWith(const UCollator* pCollator, UErrorCode* pErrorCode, const UChar* pPattern, int32_t patternLength, const UChar* pText, int32_t textLength, int32_t* pMatchedLength) +static int32_t ComplexEndsWith(SortHandle* pSortHandle, const UChar* pPattern, int32_t patternLength, const UChar* pText, int32_t textLength, int32_t options, int32_t* pMatchedLength) { int32_t result = FALSE; - UStringSearch* pSearch = usearch_openFromCollator(pPattern, patternLength, pText, textLength, pCollator, NULL, pErrorCode); - if (U_SUCCESS(*pErrorCode)) + UErrorCode err = U_ZERO_ERROR; + const UCollator* pCollator = GetCollatorFromSortHandle(pSortHandle, options, &err); + if (!U_SUCCESS(err)) { - int32_t idx = usearch_last(pSearch, pErrorCode); - if (idx != USEARCH_DONE) - { - int32_t matchEnd = idx + usearch_getMatchedLength(pSearch); - assert(matchEnd <= textLength); + return result; + } - if (matchEnd == textLength) - { - result = TRUE; - } - else - { - int32_t remainingStringLength = textLength - matchEnd; + UStringSearch* pSearch; + int32_t searchCacheSlot = GetSearchIteratorUsingCollator(pSortHandle, pCollator, pPattern, patternLength, pText, textLength, options, &pSearch); + if (searchCacheSlot < 0) + { + return result; + } - result = CanIgnoreAllCollationElements(pCollator, pText + matchEnd, remainingStringLength); - } + int32_t idx = usearch_last(pSearch, &err); + if (idx != USEARCH_DONE) + { + int32_t matchEnd = idx + usearch_getMatchedLength(pSearch); + assert(matchEnd <= textLength); - if (result && pMatchedLength != NULL) - { - // adjust matched length to account for all the elements we implicitly consumed at end of string - *pMatchedLength = textLength - idx; - } + if (matchEnd == textLength) + { + result = TRUE; + } + else + { + int32_t remainingStringLength = textLength - matchEnd; + + result = CanIgnoreAllCollationElements(pCollator, pText + matchEnd, remainingStringLength); } - usearch_close(pSearch); + if (result && pMatchedLength != NULL) + { + // adjust matched length to account for all the elements we implicitly consumed at end of string + *pMatchedLength = textLength - idx; + } } + RestoreSearchHandle(pSortHandle, pSearch, searchCacheSlot); + return result; } @@ -819,6 +1035,11 @@ int32_t GlobalizationNative_EndsWith( int32_t options, int32_t* pMatchedLength) { + if (options > CompareOptionsIgnoreCase) + { + return ComplexEndsWith(pSortHandle, lpTarget, cwTargetLength, lpSource, cwSourceLength, options, pMatchedLength); + } + UErrorCode err = U_ZERO_ERROR; const UCollator* pCollator = GetCollatorFromSortHandle(pSortHandle, options, &err); @@ -826,14 +1047,7 @@ int32_t GlobalizationNative_EndsWith( { return FALSE; } - else if (options > CompareOptionsIgnoreCase) - { - return ComplexEndsWith(pCollator, &err, lpTarget, cwTargetLength, lpSource, cwSourceLength, pMatchedLength); - } - else - { - return SimpleAffix(pCollator, &err, lpTarget, cwTargetLength, lpSource, cwSourceLength, FALSE, pMatchedLength); - } + return SimpleAffix(pCollator, &err, lpTarget, cwTargetLength, lpSource, cwSourceLength, FALSE, pMatchedLength); } int32_t GlobalizationNative_GetSortKey( diff --git a/src/libraries/Native/Unix/System.Globalization.Native/pal_icushim_internal.h b/src/libraries/Native/Unix/System.Globalization.Native/pal_icushim_internal.h index abe0f38..71c640b 100644 --- a/src/libraries/Native/Unix/System.Globalization.Native/pal_icushim_internal.h +++ b/src/libraries/Native/Unix/System.Globalization.Native/pal_icushim_internal.h @@ -142,7 +142,9 @@ PER_FUNCTION_BLOCK(usearch_first, libicui18n) \ PER_FUNCTION_BLOCK(usearch_getMatchedLength, libicui18n) \ PER_FUNCTION_BLOCK(usearch_last, libicui18n) \ - PER_FUNCTION_BLOCK(usearch_openFromCollator, libicui18n) + PER_FUNCTION_BLOCK(usearch_openFromCollator, libicui18n) \ + PER_FUNCTION_BLOCK(usearch_setPattern, libicui18n) \ + PER_FUNCTION_BLOCK(usearch_setText, libicui18n) #if HAVE_SET_MAX_VARIABLE #define FOR_ALL_SET_VARIABLE_ICU_FUNCTIONS \ @@ -280,5 +282,8 @@ FOR_ALL_ICU_FUNCTIONS #define usearch_getMatchedLength(...) usearch_getMatchedLength_ptr(__VA_ARGS__) #define usearch_last(...) usearch_last_ptr(__VA_ARGS__) #define usearch_openFromCollator(...) usearch_openFromCollator_ptr(__VA_ARGS__) +#define usearch_reset(...) usearch_reset_ptr(__VA_ARGS__) +#define usearch_setPattern(...) usearch_setPattern_ptr(__VA_ARGS__) +#define usearch_setText(...) usearch_setText_ptr(__VA_ARGS__) #endif // !defined(STATIC_ICU) diff --git a/src/libraries/Native/Unix/System.Globalization.Native/pal_icushim_internal_android.h b/src/libraries/Native/Unix/System.Globalization.Native/pal_icushim_internal_android.h index 15ce08d..2b1ed09 100644 --- a/src/libraries/Native/Unix/System.Globalization.Native/pal_icushim_internal_android.h +++ b/src/libraries/Native/Unix/System.Globalization.Native/pal_icushim_internal_android.h @@ -513,4 +513,6 @@ int32_t usearch_first(UStringSearch * strsrch, UErrorCode * status); int32_t usearch_getMatchedLength(const UStringSearch * strsrch); int32_t usearch_last(UStringSearch * strsrch, UErrorCode * status); UStringSearch * usearch_openFromCollator(const UChar * pattern, int32_t patternlength, const UChar * text, int32_t textlength, const UCollator * collator, UBreakIterator * breakiter, UErrorCode * status); +void usearch_setPattern(UStringSearch * strsrch, const UChar * pattern, int32_t patternlength, UErrorCode * status); +void usearch_setText(UStringSearch * strsrch, const UChar * text, int32_t textlength, UErrorCode * status); void ucol_setMaxVariable(UCollator * coll, UColReorderCode group, UErrorCode * pErrorCode); diff --git a/src/libraries/System.Runtime/tests/System/StringTests.cs b/src/libraries/System.Runtime/tests/System/StringTests.cs index 88f77c6..be88f38 100644 --- a/src/libraries/System.Runtime/tests/System/StringTests.cs +++ b/src/libraries/System.Runtime/tests/System/StringTests.cs @@ -9,6 +9,7 @@ using System.Reflection.Emit; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; +using System.Threading.Tasks; using Microsoft.DotNet.RemoteExecutor; using Xunit; @@ -1775,5 +1776,63 @@ namespace System.Tests Assert.True(rom.Span.SequenceEqual(rom.TrimEnd().Span)); } } + + [OuterLoop] + [Theory] + [InlineData(CompareOptions.None)] + [InlineData(CompareOptions.IgnoreCase)] + [InlineData(CompareOptions.IgnoreNonSpace)] + [InlineData(CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace)] + public void TestStringSearchCacheSynchronization(CompareOptions options) + { + int parallelism = Environment.ProcessorCount / 2; + if (Environment.ProcessorCount == 0) // 1 processor case + { + return; + } + + string source = "Abcdefgh Abcdefgh Abcdefgh Abcdefgh Abcdefgh Abcdefgh Abcdefgh Abcdefgh Abcdefgh Abcdefgh Abcdefgh Abcdefgh Abcdefgh Abcdefgh Abcdefgh Abcdefgh Abcdefgh Abcdefgh Abcdefgh Abcdefgh Abcdefgh " + + "Abcdefgh Abcdefgh Abcdefgh Abcdefgh Abcdefgh Abcdefgh Abcdefgh Abcdefgh Abcdefgh Abcdefgh Abcdefgh Abcdefgh Abcdefgh Abcdefgh Abcdefgh Abcdefgh Abcdefgh Abcdefgh Abcdefgh Abcdefgh Abcdefgh " + + "Abcdefgh Abcdefgh Abcdefgh Abcdefgh Abcdefgh Abcdefgh Abcdefgh Abcdefgh Abcdefgh Abcdefgh Abcdefgh Abcdefgh Abcdefgh Abcdefgh Abcdefgh Abcdefgh Abcdefgh Abcdefgh Abcdefgh Abcdefgh Abcdefgh " + + "сентября Abcdefgh Abcdefgh Abcdefgh Abcdefgh Abcdefgh Abcdefgh Abcdefgh Abcdefgh Abcdefgh Abcdefgh Abcdefgh Abcdefgh Abcdefgh Abcdefgh Abcdefgh Abcdefgh Abcdefgh Abcdefgh Abcdefgh Abcdefgh " + + "Abcdefgh Abcdefgh Abcdefgh Abcdefgh Abcdefgh Abcdefgh Abcdefgh Abcdefgh Abcdefgh Abcdefgh Abcdefgh Abcdefgh Abcdefgh Abcdefgh Abcdefgh Abcdefgh Abcdefgh Abcdefgh Abcdefgh Abcdefgh Abcdefgh " + + "Abcdefgh Abcdefgh Abcdefgh Abcdefgh Abcdefgh Abcdefgh Abcdefgh Abcdefgh Abcdefgh Abcdefgh Abcdefgh Abcdefgh Abcdefgh Abcdefgh Abcdefgh Abcdefgh Abcdefgh Abcdefgh Abcdefgh Abcdefgh Abcdefgh "; + + string source1 = "сентября Abcdefgh сентября "; + + string pattern = "сентября "; + string pattern1 = "сентябряnone"; + + CompareInfo ci = CultureInfo.CurrentCulture.CompareInfo; + + Task [] tasks = new Task[parallelism]; + for (int i = 0; i < parallelism; i++) + { + tasks[i] = new Task(() => + { + for (int i = 0; i < 1_00_000; i++) + { + Assert.True(ci.IndexOf(source, pattern, options) > 0, "ci.IndexOf 1"); + Assert.True(ci.LastIndexOf(source, pattern, options) > 0, "LastIndexOf 1"); + + Assert.False(ci.IndexOf(source, pattern1, options) > 0, "IndexOf 2"); + Assert.False(ci.LastIndexOf(source, pattern1, options) > 0, "LastIndexOf 2"); + + Assert.True(ci.IsPrefix(source1, pattern, options), "IsPrefix 1"); + Assert.True(ci.IsSuffix(source1, pattern, options), "IsSuffix 1"); + + Assert.False(ci.IsPrefix(source, pattern, options), "IsPrefix 2"); + Assert.False(ci.IsSuffix(source, pattern, options), "IsPrefix 2"); + } + }); + } + + for (int i = 0; i < parallelism; i++) + { + tasks[i].Start(); + } + + Task.WaitAll(tasks); + } } } -- 2.7.4