KEMBAR78
Improve Regex cache speed when cache is large by artkpv · Pull Request #27278 · dotnet/corefx · GitHub
Skip to content
This repository was archived by the owner on Jan 23, 2023. It is now read-only.

Conversation

@artkpv
Copy link
Contributor

@artkpv artkpv commented Feb 20, 2018

Fixes https://github.com/dotnet/corefx/issues/24425
Fixes https://github.com/dotnet/corefx/issues/26364

A try to speed up cache using Dictionary<CachedCodeEntryKey, CachedCodeEntry>, refs #24425

@artkpv
Copy link
Contributor Author

artkpv commented Feb 20, 2018

Results. If CacheSize 2% of input (40_000). Then 3.25 faster, +878Kb. But if CacheSize = 15 (Default) almost no impact. I don't understand but it looks like using cache more slows it down actually.

20.02.2018 Tue 19:09

N = 40_000


Original (5a57de7)

1) CacheSize 2%

   System.Text.RegularExpressions.Performance.Tests.dll          | Metric         | Unit  | Iterations |    Average |   STDEV.S |        Min |        Max
  :------------------------------------------------------------- |:-------------- |:-----:|:----------:| ----------:| ---------:| ----------:| ----------:
   System.Text.RegularExpressions.Tests.Perf_Regex_Cache.IsMatch | Duration       | msec  |     11     |    940.892 |     3.800 |    936.419 |    948.533
   System.Text.RegularExpressions.Tests.Perf_Regex_Cache.IsMatch | GC Allocations | bytes |     11     | 1.475E+008 | 38649.651 | 1.475E+008 | 1.476E+008


2) CacheSize = 15

 System.Text.RegularExpressions.Performance.Tests.dll          | Metric         | Unit  | Iterations |    Average |   STDEV.S |        Min |        Max
  :------------------------------------------------------------- |:-------------- |:-----:|:----------:| ----------:| ---------:| ----------:| ----------:
   System.Text.RegularExpressions.Tests.Perf_Regex_Cache.IsMatch | Duration       | msec  |     49     |    205.982 |     5.122 |    201.382 |    217.576
   System.Text.RegularExpressions.Tests.Perf_Regex_Cache.IsMatch | GC Allocations | bytes |     49     | 2.791E+008 | 54601.803 | 2.790E+008 | 2.792E+008


Dictionary<CachedCodeEntryKey, CachedCodeEntry> (01e16e0)

1) CacheSize 2%  - 3.255524 times faster, +878 KB

   System.Text.RegularExpressions.Performance.Tests.dll          | Metric         | Unit  | Iterations |    Average |   STDEV.S |        Min |        Max
  :------------------------------------------------------------- |:-------------- |:-----:|:----------:| ----------:| ---------:| ----------:| ----------:
   System.Text.RegularExpressions.Tests.Perf_Regex_Cache.IsMatch | Duration       | msec  |     35     |    285.896 |    10.088 |    274.010 |    311.072
   System.Text.RegularExpressions.Tests.Perf_Regex_Cache.IsMatch | GC Allocations | bytes |     35     | 1.484E+008 | 46006.727 | 1.483E+008 | 1.485E+008

2) CacheSize = 15

   System.Text.RegularExpressions.Performance.Tests.dll          | Metric         | Unit  | Iterations |    Average |   STDEV.S |        Min |        Max
  :------------------------------------------------------------- |:-------------- |:-----:|:----------:| ----------:| ---------:| ----------:| ----------:
   System.Text.RegularExpressions.Tests.Perf_Regex_Cache.IsMatch | Duration       | msec  |     50     |    202.410 |     6.412 |    197.560 |    224.385
   System.Text.RegularExpressions.Tests.Perf_Regex_Cache.IsMatch | GC Allocations | bytes |     50     | 2.791E+008 | 46727.574 | 2.790E+008 | 2.792E+008

s_livecode.AddFirst(current);
return current.Value;
}
var entry = s_livecode_dict[key];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will do two dictionary lookups. It'd be better to use TryGetValue.

s_livecode.AddFirst(current);
return current.Value;
}
var entry = s_livecode_dict[key];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto

var last = s_livecode.Last;
s_livecode_dict.Remove(last.Value._key);
s_livecode.RemoveLast();
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: this is uses tabs instead of spaces


// it wasn't in the cache, so we'll add a new one. Shortcut out for the case where cacheSize is zero.
if (s_cacheSize != 0)
else if(s_cacheSize != 0)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: missing space between if and (

internal bool _refsInitialized = false;

internal static LinkedList<CachedCodeEntry> s_livecode = new LinkedList<CachedCodeEntry>();// the cache of code and factories that are currently loaded
internal static Dictionary<CachedCodeEntryKey, CachedCodeEntry> s_livecode_dict = new Dictionary<CachedCodeEntryKey, CachedCodeEntry>();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You might also look at getting rid of s_livecode altogether and just making the CachedCodeEntry themselves into nodes in a linked list (then storing a head and last field directly).

while (s_livecode.Count > s_cacheSize)
s_livecode.RemoveLast();
{
var last = s_livecode.Last;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: var => LinkedListNode

}
var entry = s_livecode_dict[key];
s_livecode.Remove(entry);
s_livecode.AddFirst(entry);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Each time we do this Remove/AddFirst, we end up allocating another node in the linked list. That's one of the reasons I suggest just making the entries themselves into nodes in their own list, and avoiding LinkedList<T>.

@danmoseley
Copy link
Member

Can you clarify what the purpose of the LinkedList is now that you have a Dictionary? It seems to me you can get the cached value out of the Dictionary so the list doesn't add anything. What am I missing?

@danmoseley
Copy link
Member

@artkpv is this the code you are benchmarking? https://github.com/dotnet/corefx/issues/24425#issuecomment-361280655

I am curious how much of the improvement (for your 2%) case is faster lookup in the cache, and how much is faster Regex.Match because you are using a cached regex. Maybe it doesn't matter, but if you care, you could use a profiler and see how much time was spent in cache lookup specifically.

@stephentoub
Copy link
Member

stephentoub commented Feb 20, 2018

Can you clarify what the purpose of the LinkedList is now that you have a Dictionary? It seems to me you can get the cached value out of the Dictionary so the list doesn't add anything. What am I missing?

It's an LRU cache, so you need to know the order in which the items are used.

@artkpv
Copy link
Contributor Author

artkpv commented Feb 22, 2018

@stephentoub Code style issues fixed.
About linked list built into Regex. I'm testing it. Hope it would save some space.

@danmosemsft No, it is PR #27202 . The test in this PR also.

I am curious how much of the improvement (for your 2%) case is faster lookup in the cache, and how much is faster Regex.Match because you are using a cached regex

Didn't understand. Regex.Match also uses the cache, right? Through ctor. So should be no difference. Or I miss something?

@artkpv
Copy link
Contributor Author

artkpv commented Feb 22, 2018

Here is the version for the linked list built into Regex. I'm checking the code for correctness again. The test results (2% case):

   System.Text.RegularExpressions.Performance.Tests.dll          | Metric         | Unit  | Iterations |    Average |   STDEV.S |        Min |        Max
  :------------------------------------------------------------- |:-------------- |:-----:|:----------:| ----------:| ---------:| ----------:| ----------:
   System.Text.RegularExpressions.Tests.Perf_Regex_Cache.IsMatch | Duration       | msec  |     42     |    243.406 |    19.344 |    230.520 |    343.603
   System.Text.RegularExpressions.Tests.Perf_Regex_Cache.IsMatch | GC Allocations | bytes |     42     | 1.468E+008 | 40974.308 | 1.467E+008 | 1.469E+008

@danmoseley
Copy link
Member

danmoseley commented Feb 23, 2018

macOS runs are hitting some NRE:

Unhandled Exception of Type System.NullReferenceException
Message :
System.NullReferenceException : Object reference not set to an instance of an object.
Stack Trace :
   at System.Text.RegularExpressions.Regex.CacheCode(CachedCodeEntryKey key) in /Users/dotnet-bot/j/workspace/dotnet_corefx/master/osx-TGroup_netcoreapp+CGroup_Debug+AGroup_x64+TestOuter_false_prtest/src/System.Text.RegularExpressions/src/System/Text/RegularExpressions/Regex.cs:line 1119
   at System.Text.RegularExpressions.Regex..ctor(String pattern, RegexOptions options, TimeSpan matchTimeout, Boolean addToCache) in /Users/dotnet-bot/j/workspace/dotnet_corefx/master/osx-TGroup_netcoreapp+CGroup_Debug+AGroup_x64+TestOuter_false_prtest/src/System.Text.RegularExpressions/src/System/Text/RegularExpressions/Regex.cs:line 218
   at System.Text.RegularExpressions.Regex.Match(String input, String pattern, RegexOptions options, TimeSpan matchTimeout) in /Users/dotnet-bot/j/workspace/dotnet_corefx/master/osx-TGroup_netcoreapp+CGroup_Debug+AGroup_x64+TestOuter_false_prtest/src/System.Text.RegularExpressions/src/System/Text/RegularExpressions/Regex.cs:line 662
   at System.Text.RegularExpressions.Regex.Match(String input, String pattern, RegexOptions options) in /Users/dotnet-bot/j/workspace/dotnet_corefx/master/osx-TGroup_netcoreapp+CGroup_Debug+AGroup_x64+TestOuter_false_prtest/src/System.Text.RegularExpressions/src/System/Text/RegularExpressions/Regex.cs:line 656
   at System.Xml.Tests.ExceptionVerifier.CompareMessages() in /Users/dotnet-bot/j/workspace/dotnet_corefx/master/osx-TGroup_netcoreapp+CGroup_Debug+AGroup_x64+TestOuter_false_prtest/src/System.Private.Xml/tests/XmlSchema/XmlSchemaValidatorApi/ExceptionVerifier.cs:line 267
   at System.Xml.Tests.ExceptionVerifier.IsExceptionOk(Exception e, Object[] IdsAndParams) in /Users/dotnet-bot/j/workspace/dotnet_corefx/master/osx-TGroup_netcoreapp+CGroup_Debug+AGroup_x64+TestOuter_false_prtest/src/System.Private.Xml/tests/XmlSchema/XmlSchemaValidatorApi/ExceptionVerifier.cs:line 324
   at System.Xml.Tests.ExceptionVerifier.IsExceptionOk(Exception e, String expectedResId, String[] paramValues) in /Users/dotnet-bot/j/workspace/dotnet_corefx/master/osx-TGroup_netcoreapp+CGroup_Debug+AGroup_x64+TestOuter_false_prtest/src/System.Private.Xml/tests/XmlSchema/XmlSchemaValidatorApi/ExceptionVerifier.cs:line 303
   at System.Xml.Tests.TCEndValidation.TestForRootLevelIdentityConstraints_Valid_IDREFMissingInvalid_IgnoreIdentityConstraintsIsSetInvalid(String validity) in /Users/dotnet-bot/j/workspace/dotnet_corefx/master/osx-TGroup_netcoreapp+CGroup_Debug+AGroup_x64+TestOuter_false_prtest/src/System.Private.Xml/tests/XmlSchema/XmlSchemaValidatorApi/Initialize_EndValidation.cs:line 601

https://mc.dot.net/#/user/artkpv/pr~2Fjenkins~2Fdotnet~2Fcorefx~2Fmaster~2F/test~2Ffunctional~2Fcli~2F/c6e809060e6a4691275c734930b508568be579e5/workItem/System.Xml.XmlSchema.XmlSchemaValidatorApi.Tests

Note that Regex tests did not hit this, suggesting we are missing coverage in the regex tests?

@artkpv
Copy link
Contributor Author

artkpv commented Feb 27, 2018

@danmosemsft I've added some unit tests though they only check that it doesn't throw. Probably we need some api for the cache. Regarding the error, I'm not sure how it happened. I'm checking the code again. I guess it was line 364 other thread changed cache size to 0 while the linked list was being updated at line 1119. So I put it into lock.

lock (s_livecode)
{
for (LinkedListNode<CachedCodeEntry> current = s_livecode.First; current != null; current = current.Next)
s_livecode.TryGetValue(key, out var entry);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In a common case (possibly the most common?), the regex will be the last used regex. Would it make sense to test whether s_livecode_first == entry (and return immediately if true) before looking up in the dictionary, etc.?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if you do this then CachePromoteLinkedListEntry would be repeating that test, so you would probably move the check out into the other caller also (CacheCode ) even though in that code path it's unlikely to be true - since we aren't asked to cache anything if it was found in the cache.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, reasonable, in case of the same regex made many times. Adding this

return current.Value;
}
CachePromoteLinkedListEntry(entry);
return entry;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit, if you want you could remove this return entry; and change the return null; to return entry;


internal static LinkedList<CachedCodeEntry> s_livecode = new LinkedList<CachedCodeEntry>();// the cache of code and factories that are currently loaded
// the cache of code and factories that are currently loaded:
internal static Dictionary<CachedCodeEntryKey, CachedCodeEntry> s_livecode = new Dictionary<CachedCodeEntryKey, CachedCodeEntry>();
Copy link
Member

@danmoseley danmoseley Feb 27, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the default is going to be very common, and is not too large, you probably want to pre size:
internal static Dictionary<CachedCodeEntryKey, CachedCodeEntry> s_livecode = new Dictionary<CachedCodeEntryKey, CachedCodeEntry>(s_cacheSize);

@danmoseley
Copy link
Member

This will conflict with #27473. That's fine whoever is last has to fix a simple conflict.

It would probably make sense to run tests on this several times before merging (@dotnet-bot followed by test this please) to help shake out any races - although with everything behind a lock, there should not be an issue.

[Fact]
public void Ctor_Cache_Shrink_cache_does_not_throw()
{
int originalCacheSize = Regex.CacheSize;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: preferably resetting the cache size should be in a finally block, in case one test fails it should not break the others.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

@artkpv
Copy link
Contributor Author

artkpv commented Mar 24, 2018

Tested it again. Results below. Added unit test for the limit, assertions. The case that runs a bit slower (~40 msec) is 40_000, with 1600 unique, 15 cache. Others either run the same or quicker (large cache).

Update. Next i'm adding some checks on the switch as i'm not sure about this.

Master (320d57a)  / this PR (528fdc5) (with 'X' at end)
                                                                                        | Metric         | Unit  | Iterations |    Average |    STDEV.S |        Min |        Max
  :------------------------------------------------------------------------------------ |:-------------- |:-----:|:----------:| ----------:| ----------:| ----------:| ----------:
   Perf_Regex.Match                                                                     | Duration       | msec  |     51     |    196.566 |     14.792 |    189.138 |    285.191
   Perf_Regex.Match                                                                     | GC Allocations | bytes |     51     | 1.803E+008 |  43019.333 | 1.802E+008 | 1.804E+008
   Perf_Regex.Match                                                                    X| Duration       | msec  |     54     |    186.347 |      5.003 |    181.403 |    212.251
   Perf_Regex.Match                                                                    X| GC Allocations | bytes |     54     | 1.931E+008 |  45806.833 | 1.930E+008 | 1.931E+008
   Perf_Regex_Cache.IsMatch(total: 40000, unique: 1600, cacheSize: 15)                  | Duration       | msec  |     50     |    201.587 |      2.371 |    197.298 |    209.324
   Perf_Regex_Cache.IsMatch(total: 40000, unique: 1600, cacheSize: 15)                  | GC Allocations | bytes |     50     | 2.695E+008 |  36735.119 | 2.694E+008 | 2.696E+008
   Perf_Regex_Cache.IsMatch(total: 40000, unique: 1600, cacheSize: 15)                 X| Duration       | msec  |     42     |    241.903 |      4.679 |    238.809 |    268.423
   Perf_Regex_Cache.IsMatch(total: 40000, unique: 1600, cacheSize: 15)                 X| GC Allocations | bytes |     42     | 2.682E+008 |  40061.755 | 2.681E+008 | 2.683E+008
   Perf_Regex_Cache.IsMatch(total: 40000, unique: 1600, cacheSize: 3200)                | Duration       | msec  |     16     |    661.390 |     30.149 |    645.304 |    772.256
   Perf_Regex_Cache.IsMatch(total: 40000, unique: 1600, cacheSize: 3200)                | GC Allocations | bytes |     16     | 4.196E+006 |  27129.164 | 4.182E+006 | 4.298E+006
   Perf_Regex_Cache.IsMatch(total: 40000, unique: 1600, cacheSize: 3200)               X| Duration       | msec  |    324     |     30.811 |      1.501 |     28.781 |     42.861
   Perf_Regex_Cache.IsMatch(total: 40000, unique: 1600, cacheSize: 3200)               X| GC Allocations | bytes |    324     | 4.196E+006 |  30671.967 | 4.182E+006 | 4.299E+006
   Perf_Regex_Cache.IsMatch(total: 40000, unique: 1600, cacheSize: 800)                 | Duration       | msec  |     11     |    960.674 |     36.247 |    933.295 |   1064.315
   Perf_Regex_Cache.IsMatch(total: 40000, unique: 1600, cacheSize: 800)                 | GC Allocations | bytes |     11     | 1.425E+008 |  49772.995 | 1.424E+008 | 1.426E+008
   Perf_Regex_Cache.IsMatch(total: 40000, unique: 1600, cacheSize: 800)                X| Duration       | msec  |     38     |    270.377 |      8.358 |    253.366 |    284.353
   Perf_Regex_Cache.IsMatch(total: 40000, unique: 1600, cacheSize: 800)                X| GC Allocations | bytes |     38     | 1.419E+008 |  51099.027 | 1.418E+008 | 1.420E+008
   Perf_Regex_Cache.IsMatch(total: 40000, unique: 7, cacheSize: 0)                      | Duration       | msec  |     95     |    105.896 |      1.610 |    103.494 |    113.347
   Perf_Regex_Cache.IsMatch(total: 40000, unique: 7, cacheSize: 0)                      | GC Allocations | bytes |     95     | 1.772E+008 |  44173.852 | 1.771E+008 | 1.773E+008
   Perf_Regex_Cache.IsMatch(total: 40000, unique: 7, cacheSize: 0)                     X| Duration       | msec  |     97     |    103.484 |      3.228 |    101.063 |    124.970
   Perf_Regex_Cache.IsMatch(total: 40000, unique: 7, cacheSize: 0)                     X| GC Allocations | bytes |     97     | 1.771E+008 |  53441.500 | 1.771E+008 | 1.772E+008
   Perf_Regex_Cache.IsMatch(total: 400000, unique: 1, cacheSize: 15)                    | Duration       | msec  |     92     |    108.707 |      4.836 |    104.958 |    133.435
   Perf_Regex_Cache.IsMatch(total: 400000, unique: 1, cacheSize: 15)                    | GC Allocations | bytes |     92     | 4.197E+007 |  52337.860 | 4.190E+007 | 4.206E+007
   Perf_Regex_Cache.IsMatch(total: 400000, unique: 1, cacheSize: 15)                   X| Duration       | msec  |    112     |     89.817 |      1.242 |     87.582 |     93.743
   Perf_Regex_Cache.IsMatch(total: 400000, unique: 1, cacheSize: 15)                   X| GC Allocations | bytes |    112     | 4.197E+007 |  52310.763 | 4.189E+007 | 4.206E+007
   Perf_Regex_Cache.IsMatch(total: 400000, unique: 7, cacheSize: 15)                    | Duration       | msec  |     75     |    134.144 |      2.335 |    131.859 |    145.196
   Perf_Regex_Cache.IsMatch(total: 400000, unique: 7, cacheSize: 15)                    | GC Allocations | bytes |     75     | 4.197E+007 |  49363.161 | 4.190E+007 | 4.201E+007
   Perf_Regex_Cache.IsMatch(total: 400000, unique: 7, cacheSize: 15)                   X| Duration       | msec  |     73     |    137.299 |      2.123 |    133.994 |    145.484
   Perf_Regex_Cache.IsMatch(total: 400000, unique: 7, cacheSize: 15)                   X| GC Allocations | bytes |     73     | 4.197E+007 |  51885.981 | 4.187E+007 | 4.207E+007
   Perf_Regex_Cache.IsMatch_Multithreading(total: 40000, unique: 1600, cacheSize: 15)   | Duration       | msec  |     18     |    582.286 |     49.795 |    536.098 |    726.932
   Perf_Regex_Cache.IsMatch_Multithreading(total: 40000, unique: 1600, cacheSize: 15)   | GC Allocations | bytes |     18     | 1.870E+009 | 3.100E+007 | 1.748E+009 | 1.884E+009
   Perf_Regex_Cache.IsMatch_Multithreading(total: 40000, unique: 1600, cacheSize: 15)  X| Duration       | msec  |     15     |    707.802 |     11.235 |    697.737 |    745.483
   Perf_Regex_Cache.IsMatch_Multithreading(total: 40000, unique: 1600, cacheSize: 15)  X| GC Allocations | bytes |     15     | 1.725E+009 | 8.624E+006 | 1.712E+009 | 1.739E+009
   Perf_Regex_Cache.IsMatch_Multithreading(total: 40000, unique: 1600, cacheSize: 3200) | Duration       | msec  |     3      |   3687.699 |      8.964 |   3677.932 |   3695.551
   Perf_Regex_Cache.IsMatch_Multithreading(total: 40000, unique: 1600, cacheSize: 3200) | GC Allocations | bytes |     3      | 1.687E+007 |  59230.675 | 1.683E+007 | 1.693E+007
   Perf_Regex_Cache.IsMatch_Multithreading(total: 40000, unique: 1600, cacheSize: 3200)X| Duration       | msec  |    118     |     84.756 |      7.108 |     74.251 |    103.732
   Perf_Regex_Cache.IsMatch_Multithreading(total: 40000, unique: 1600, cacheSize: 3200)X| GC Allocations | bytes |    118     | 1.916E+007 | 431921.780 | 1.807E+007 | 2.020E+007
   Perf_Regex_Cache.IsMatch_Multithreading(total: 40000, unique: 1600, cacheSize: 800)  | Duration       | msec  |     3      |   4702.343 |      6.966 |   4694.344 |   4707.080
   Perf_Regex_Cache.IsMatch_Multithreading(total: 40000, unique: 1600, cacheSize: 800)  | GC Allocations | bytes |     3      | 5.691E+008 | 459223.586 | 5.686E+008 | 5.695E+008
   Perf_Regex_Cache.IsMatch_Multithreading(total: 40000, unique: 1600, cacheSize: 800) X| Duration       | msec  |     12     |    874.075 |     19.358 |    850.969 |    904.756
   Perf_Regex_Cache.IsMatch_Multithreading(total: 40000, unique: 1600, cacheSize: 800) X| GC Allocations | bytes |     12     | 8.655E+008 | 6.594E+006 | 8.542E+008 | 8.761E+008
   Perf_Regex_Cache.IsMatch_Multithreading(total: 40000, unique: 7, cacheSize: 0)       | Duration       | msec  |     31     |    332.049 |      8.146 |    318.079 |    356.674
   Perf_Regex_Cache.IsMatch_Multithreading(total: 40000, unique: 7, cacheSize: 0)       | GC Allocations | bytes |     31     | 1.481E+009 | 1.387E+007 | 1.450E+009 | 1.512E+009
   Perf_Regex_Cache.IsMatch_Multithreading(total: 40000, unique: 7, cacheSize: 0)      X| Duration       | msec  |     31     |    327.905 |      7.025 |    313.534 |    344.176
   Perf_Regex_Cache.IsMatch_Multithreading(total: 40000, unique: 7, cacheSize: 0)      X| GC Allocations | bytes |     31     | 1.507E+009 | 1.509E+007 | 1.469E+009 | 1.541E+009
   Perf_Regex_Cache.IsMatch_Multithreading(total: 400000, unique: 1, cacheSize: 15)     | Duration       | msec  |     17     |    625.968 |      7.875 |    607.565 |    642.039
   Perf_Regex_Cache.IsMatch_Multithreading(total: 400000, unique: 1, cacheSize: 15)     | GC Allocations | bytes |     17     | 1.717E+009 | 1.878E+007 | 1.694E+009 | 1.762E+009
   Perf_Regex_Cache.IsMatch_Multithreading(total: 400000, unique: 1, cacheSize: 15)    X| Duration       | msec  |     20     |    516.408 |      8.847 |    496.001 |    529.156
   Perf_Regex_Cache.IsMatch_Multithreading(total: 400000, unique: 1, cacheSize: 15)    X| GC Allocations | bytes |     20     | 1.980E+009 | 1.704E+007 | 1.934E+009 | 1.996E+009
   Perf_Regex_Cache.IsMatch_Multithreading(total: 400000, unique: 7, cacheSize: 15)     | Duration       | msec  |     15     |    671.842 |      7.387 |    651.555 |    683.594
   Perf_Regex_Cache.IsMatch_Multithreading(total: 400000, unique: 7, cacheSize: 15)     | GC Allocations | bytes |     15     | 4.159E+008 | 2.998E+006 | 4.077E+008 | 4.216E+008
   Perf_Regex_Cache.IsMatch_Multithreading(total: 400000, unique: 7, cacheSize: 15)    X| Duration       | msec  |     17     |    619.960 |      5.971 |    609.060 |    627.484
   Perf_Regex_Cache.IsMatch_Multithreading(total: 400000, unique: 7, cacheSize: 15)    X| GC Allocations | bytes |     17     | 6.115E+008 | 3.338E+006 | 6.064E+008 | 6.173E+008

@ViktorHofer
Copy link
Member

I'm currently looking over your changes and saw some code styling nits and a missing license header. I hope you don't mind that I pushed into your branch.

@ViktorHofer
Copy link
Member

I think you went a bit overboard with the amount of Debug.Assert calls. I recommend either removing a few which don't bring any value or adding messages to the calls which don't use the string overload.

}

lock (s_livecode)
return TryGetCacheValueSmall(key, out entry);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Attributing methods with AggressiveInlining can be very dangerous and regress perf in corner cases which are usually not tested. I factored out the retrieval for small caches so that the the caller's native size stays sane.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IL Size is now 30 bytes.

@ViktorHofer
Copy link
Member

ViktorHofer commented Mar 26, 2018

Note: The numbers for the concurrent tests could be spoiled because of the task creation inside the tight loop. I removed the costly LINQ operations which should remove some noise.

@ViktorHofer
Copy link
Member

ViktorHofer commented Mar 26, 2018

Results with BenchmarkDotNet:

BenchmarkDotNet=v0.10.13, OS=Windows 10.0.17128
Intel Core i7-6600U CPU 2.60GHz (Skylake), 1 CPU, 4 logical cores and 2 physical cores
Frequency=2742185 Hz, Resolution=364.6727 ns, Timer=TSC
  [Host] : .NET Core ? (CoreCLR 4.6.26228.08, CoreFX 4.6.26424.0), 64bit RyuJIT

Job=.NET Core 2.1 regex  Runtime=Core  Toolchain=InProcessToolchain  

Before

Method Param Mean Error StdDev Gen 0 Gen 1 Gen 2 Allocated
IsMatch (Patterns: 200000, Unique patterns: 1, Cache Size: 15) 61.10 ms 0.4231 ms 0.3957 ms 10312.5000 1500.0000 500.0000 21880.45 KB
IsMatch_Multithreading (Patterns: 200000, Unique patterns: 1, Cache Size: 15) 169.59 ms 3.2990 ms 3.5299 ms 71750.0000 1500.0000 500.0000 1564.09 KB
IsMatch (Patterns: 200000, Unique patterns: 7, Cache Size: 15) 77.55 ms 0.5605 ms 0.4969 ms 10312.5000 1500.0000 500.0000 21907.97 KB
IsMatch_Multithreading (Patterns: 200000, Unique patterns: 7, Cache Size: 15) 82.31 ms 0.6045 ms 0.5048 ms 22750.0000 1500.0000 500.0000 1565.55 KB
IsMatch (Patterns: 40000, Unique patterns: 600, Cache Size: 1200) 181.14 ms 2.9748 ms 2.6371 ms 1500.0000 625.0000 62.5000 8226.33 KB
IsMatch_Multithreading (Patterns: 40000, Unique patterns: 600, Cache Size: 1200) 253.90 ms 2.5297 ms 2.3662 ms 1625.0000 750.0000 62.5000 544.32 KB
IsMatch (Patterns: 40000, Unique patterns: 600, Cache Size: 15) 270.95 ms 4.2214 ms 3.9487 ms 116875.0000 437.5000 62.5000 239975.34 KB
IsMatch_Multithreading (Patterns: 40000, Unique patterns: 600, Cache Size: 15) 235.94 ms 2.0884 ms 1.8513 ms 118562.5000 437.5000 62.5000 544.32 KB
IsMatch (Patterns: 40000, Unique patterns: 600, Cache Size: 300) 302.76 ms 5.9268 ms 6.0864 ms 20750.0000 10312.5000 125.0000 127025.9 KB
IsMatch_Multithreading (Patterns: 40000, Unique patterns: 600, Cache Size: 300) 353.62 ms 1.1618 ms 0.9702 ms 20937.5000 10437.5000 125.0000 544.32 KB
IsMatch (Patterns: 40000, Unique patterns: 7, Cache Size: 0) 114.54 ms 2.1854 ms 1.9373 ms 83437.5000 625.0000 62.5000 171251.99 KB
IsMatch_Multithreading (Patterns: 40000, Unique patterns: 7, Cache Size: 0) 242.56 ms 4.4528 ms 4.1651 ms 84125.0000 187.5000 62.5000 315.55 KB

After

Method Param Mean Error StdDev Gen 0 Gen 1 Gen 2 Allocated
IsMatch (Patterns: 200000, Unique patterns: 1, Cache Size: 15) 54.11 ms 0.3388 ms 0.3169 ms 10312.5000 1500.0000 500.0000 21880.26 KB
IsMatch_Multithreading (Patterns: 200000, Unique patterns: 1, Cache Size: 15) 158.50 ms 3.4490 ms 3.8335 ms 73750.0000 1500.0000 500.0000 1564.09 KB
IsMatch (Patterns: 200000, Unique patterns: 7, Cache Size: 15) 79.58 ms 0.9665 ms 0.9040 ms 10312.5000 1500.0000 500.0000 21908.55 KB
IsMatch_Multithreading (Patterns: 200000, Unique patterns: 7, Cache Size: 15) 77.68 ms 0.5648 ms 0.5283 ms 31062.5000 1562.5000 500.0000 1565.55 KB
IsMatch (Patterns: 40000, Unique patterns: 600, Cache Size: 1200) 31.29 ms 0.5516 ms 0.5160 ms 1437.5000 687.5000 125.0000 8207.58 KB
IsMatch_Multithreading (Patterns: 40000, Unique patterns: 600, Cache Size: 1200) 22.30 ms 0.3647 ms 0.3411 ms 1656.2500 781.2500 125.0000 544.32 KB
IsMatch (Patterns: 40000, Unique patterns: 600, Cache Size: 15) 225.65 ms 2.3694 ms 2.2163 ms 70250.0000 24062.5000 62.5000 238756.34 KB
IsMatch_Multithreading (Patterns: 40000, Unique patterns: 600, Cache Size: 15) 146.49 ms 0.9696 ms 0.9070 ms 71250.0000 24375.0000 62.5000 544.32 KB
IsMatch (Patterns: 40000, Unique patterns: 600, Cache Size: 300) 164.84 ms 2.3444 ms 2.0783 ms 20562.5000 8062.5000 2437.5000 126391.93 KB
IsMatch_Multithreading (Patterns: 40000, Unique patterns: 600, Cache Size: 300) 115.21 ms 2.2527 ms 2.3133 ms 20937.5000 8437.5000 2312.5000 544.32 KB
IsMatch (Patterns: 40000, Unique patterns: 7, Cache Size: 0) 117.00 ms 1.4168 ms 1.3253 ms 83437.5000 687.5000 62.5000 171251.99 KB
IsMatch_Multithreading (Patterns: 40000, Unique patterns: 7, Cache Size: 0) 237.28 ms 4.5083 ms 5.1918 ms 84125.0000 187.5000 62.5000 315.55 KB

Run yourself

  1. Build corefx in release mode .\build -release once.
  2. Copy code from: https://gist.github.com/ViktorHofer/ff901113f227db57813ed9cc29f4fc05 to bin/runtime/netcoreapp-Windows_NT-Release-x64/regex-cache-perf.cs.
  3. Copy both BenchmarkDotNet assemblies (BDN.dll & BDN.Core.dll) into the same folder.
  4. Incremental changes:
cd corefx
msbuild /t:Rebuild /p:ConfigurationGroup=Release src/System.Text.RegularExpressions/src/
cd bin/runtime/netcoreapp-Windows_NT-Release-x64/
.\CoreRun ..\..\..\tools\csc.dll /noconfig /optimize /r:System.Private.Corelib.dll /r:System.Runtime.dll /r:System.Runtime.Extensions.dll /r:System.Console.dll /r:System.Text.RegularExpressions.dll /r:System.Collections.dll /r:BenchmarkDotNet.dll /r:BenchmarkDotNet.Core.dll /out:regex-cache-perf.dll regex-cache-perf.cs
.\CoreRun regex-cache-perf.dll

@ViktorHofer
Copy link
Member

The improvements especially in multi threaded scenarios are very promising. I'm very happy to merge this asap.

@danmoseley
Copy link
Member

I'm particularly glad to see wins even for the default cache size. 🍾 🥂

@ViktorHofer
Copy link
Member

@dotnet-bot test Windows x64 Debug Build

@danmoseley danmoseley merged commit a96dd02 into dotnet:master Mar 27, 2018
@danmoseley
Copy link
Member

Nice work @artkpv appreciate it.

@danmoseley
Copy link
Member

Hmm @ViktorHofer I interpreted your comment above as a signoff but now I see you didn't actually hit signoff. All good?

@ViktorHofer
Copy link
Member

My intention was to wait for @artkpv's response but we can tackle further optimization in a separate PR.

@artkpv
Copy link
Contributor Author

artkpv commented Mar 27, 2018

Great to see it merged! 😄👏 Really glad it ships and runs in Corefx! @ViktorHofer thanks for the testing and those commits.

@ViktorHofer
Copy link
Member

ViktorHofer commented Mar 27, 2018

If you want to help further we have some other open issues for Regex i.e. https://github.com/dotnet/corefx/issues/7569 & https://github.com/dotnet/corefx/issues/26787

@karelz karelz added this to the 2.1.0 milestone Mar 27, 2018
artkpv added a commit to artkpv/corefx that referenced this pull request Oct 16, 2018
picenka21 pushed a commit to picenka21/runtime that referenced this pull request Feb 18, 2022
* Regex Cache changed to use Dict. PerTest added

* Regex perf test for cache, refs dotnet/corefx#24425

* Getting CacheSize back in case of exception

* Adjusting input not to hit cache each call

* Code style issues

* Linked list built into Regex

* Cache size change in critial region. Adding tests

* Check the first first. Default init. Refactoring. Tests fixed

* Optimizations: reordering checks, methods calls

* Eq op fixed. Comments fixed

* Avoiding lock, refs dotnet/corefx#26364

* Merge. Removing message suppresion

* Linked list fix

* Indention fixed. Commenting

* Unit tests fixed

* Avoiding extra check. Access to public in internal class. Commenting.

* Adding perf tests for various CacheSize

* Perf test: cleaning up test

* Testing Cache.Equals. Enhancing tests. Refactoring: renaming

* Tests to run in other process

* (To continue) Addressing most frequent cases to avoid lock

* Perf tests enhanced. Addressing most frequent cases to avoid lock

* Avoiding lock. Tests turning.

* Assertions added. Check added for Most Recent inside lock. Removing nullifying

* Perf test cases added, tuned

* Perf test: disable cache to get clearer results

* Fixing assertion error

* Perf test: multithreading

* clean up

* Switching to Dictionary at 10, using Linked list before

* Limit to 11

* Bug fix: clearing prev hashes. Switch at 10

* Fixing possible bug with CacheSize change. Adding tests

* Removing assertion

* Check for cache size change near limit

* License header, code styling, nits

* Realign inline methods for Cache retrieval

* right size multi threaded bench costs per thread

* Fix cache num retrieval on netfx


Commit migrated from dotnet/corefx@a96dd02
picenka21 pushed a commit to picenka21/runtime that referenced this pull request Feb 18, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants