Skip to content
This repository has been archived by the owner on Jan 23, 2023. It is now read-only.

CoreFx #15622 Dictionary<TKey, TValue>.Remove(TKey, out TValue) #10203

Merged
merged 1 commit into from
Apr 11, 2017

Conversation

WinCPP
Copy link

@WinCPP WinCPP commented Mar 15, 2017

New API to return the value associated with key being removed, if the key is present.

@karelz @danmosemsft Kindly review.

@@ -592,11 +592,19 @@ private void Resize(int newSize, bool forceNewHashCodes)

public bool Remove(TKey key)
{
TValue unused;
return Remove(key, out unused);
Copy link
Author

Choose a reason for hiding this comment

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

In C# 7.0, this might be a place to use out * in place of out unused.

Copy link
Member

Choose a reason for hiding this comment

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

That doesn't seem to compile in our compiler right now. If it did, I think it's fine.

Copy link

Choose a reason for hiding this comment

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

The actual C# 7.0 syntax is out var _. Using that seems to compile fine for me.

@@ -614,6 +622,9 @@ public bool Remove(TKey key)
{
entries[last].next = entries[i].next;
}

value = entries[i].value;
Copy link
Member

Choose a reason for hiding this comment

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

It might be slightly clearer if this line went directly after the if on line 615

if (key == null)
{
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key);
}

value = default(TValue);
Copy link
Member

Choose a reason for hiding this comment

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

this could move just before the return false.

@@ -614,6 +622,9 @@ public bool Remove(TKey key)
{
entries[last].next = entries[i].next;
}

value = entries[i].value;
Copy link

Choose a reason for hiding this comment

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

This implementation of public bool Remove(TKey key, out TValue value) penalizes many users of public bool Remove(TKey key) by introducing an unnecessary copy (and initialization) of the value.

Copy link
Member

Choose a reason for hiding this comment

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

@mikedn Any suggestions? A branch? Duplicate the body of the function?

Copy link

Choose a reason for hiding this comment

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

Duplicating the code might be the only option that doesn't impact performance. But it too has its cost since we're looking at a generic type. Using a branch to avoid the unnecessary copy might be a reasonable compromise.

But before doing any of that the performance impact should be measured.

Copy link
Author

Choose a reason for hiding this comment

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

@danmosemsft @mikedn Thanks for the review comments. I will work on getting the performance data. This review gives me a close and significant insight into working of C# and how to think differently when wearing a C# vs C++ hat.

Copy link
Author

@WinCPP WinCPP Mar 16, 2017

Choose a reason for hiding this comment

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

@mikedn hmm. A few questions, mainly as a learning for me...

Duplicating the code might be the only option that doesn't impact performance. But it too has its cost since we're looking at a generic type.

  1. Are we referring to maintenance cost? But then what is special in context of generic type... Seems there is more here... Please share.

Using a branch to avoid the unnecessary copy might be a reasonable compromise.

  1. In non-duplication way, I'm not able to understand how we could avoid assignment at line 626, since we have to squirrel away the value before being overwritten. I think I am not able to understand what is meant by branch here. Appreciate your inputs.

  2. I was curious why the C# compiler would not inline entire Remove(key, out value) into the Remove(key) and distill off the unnecessary copies via intermediate code optimizations. For long I have always wondered why the chained constructors also won't inline (as seen in IL for even Release mode).

Performance data points I will collect
a. Old Remove implementation
b. Nested Remove implementation as in the PR with code re-org.
c. Branched version (but I need to be sure I understand what is meant by branch in this context).

Thanks!

Copy link

Choose a reason for hiding this comment

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

Are we referring to maintenance cost? But then what is special in context of generic type...

Maintenance cost too since we'll end up with 2 very similar pieces of code. But this is a generic type, this means that if you duplicate that code you're not adding just a single method, you're adding many more - one for each Dictionary instantiation where TValue is a value type (e.g. one for Dictionary<int, int>, another one for Dictionary<int, bool> and so on).

In non-duplication way, I'm not able to understand how we could avoid assignment at line 626...

I don't know exactly what @danmosemsft had in mind when he said branch but one possible option is to pass a bool parameter to the method indicating whether to perform the copy or not. That would imply moving all this code to a new private method and having both Remove overloads call that method.

I was curious why the C# compiler would not inline entire Remove(key, out value) into the...

The C# compiler doesn't do inlining, that's the job of the JIT compiler since inlining normally needs to take into account various "low level" aspects (e.g. native code size, calling convention). Anyway, there's too much code to inline.

Performance data points I will collect

Make sure to measure the case of large value types. I don't expect this change to have a measurable impact on Dictionary<int, int> but the story may be different for something like Dictionary<int, Matrix>.

@danmoseley
Copy link
Member

danmoseley commented Mar 15, 2017

@WinCPP could you please do some ad-hoc performance measurements? A simple console app is fine.

This should help: https://github.com/dotnet/coreclr/blob/master/Documentation/workflow/UsingYourBuild.md
(Make sure to add some Console.WriteLine into the corelib somewhere to be certain it's your modified version getting used. )

Even easier, just install 2.0 and paste in the Dictionary code into your own code -- both the old and new implementations (I'd try a branch to avoid the copy). Make sure to build release.

Then I'd set up some dictionaries with different numbers of entries and confirm the timings with each implementation. Timing with Stopwatch is fine, and you'd probably do a bunch of iterations and drop outliers and get the standard deviation.

Perhaps @mikedn can point to a gist of code someone else has used for this purpose. I haven't got one handy.

@danmoseley
Copy link
Member

@safern
Copy link
Member

safern commented Mar 16, 2017

Or even something simple like this one:

#9923 (comment)

@WinCPP
Copy link
Author

WinCPP commented Mar 16, 2017

Thanks @danmosemsft @safern for the guidance. I will work on the performance part.

@WinCPP
Copy link
Author

WinCPP commented Mar 16, 2017

For the performance testing, I want to use my local clr + corefx. I'm stuck in that. Please help. I want to use local CoreCLR because it has implementation and CoreFx because it has facade.

  1. I built CoreCLR (build.cmd x64 Release) and configured CoreFx to use it by following the steps in "Testing with private CoreCLR bits". I had passed -release to .\build.cmd while building CoreFX.
  2. As mentioned in "Using your local CoreFx build", I modified the .csproj and Nuget.config to pick up Microsoft.Private.CoreFx.NETCoreApp from my release folder.

But the dotnet restore fails with following errors, which I am not able to solve. Please help. Sorry for dumping entire output, but I'm totally clueless :-(

D:\Progs\C#Progs\DictPerf>e:\dotnet2.0\dotnet.exe restore
  Restoring packages for D:\Progs\C#Progs\DictPerf\DictPerf.csproj...
e:\dotnet2.0\sdk\2.0.0-preview1-005440\NuGet.targets(97,5): error : Unable to resolve 'Microsoft.NETCore.Platforms (>= 2.0.0-beta-25211-0)' for '.NETCoreApp,Version=v2.0'. [D:\Progs\C#Progs\DictPerf\DictPerf.csproj]
e:\dotnet2.0\sdk\2.0.0-preview1-005440\NuGet.targets(97,5): error : Unable to resolve 'Microsoft.NETCore.Platforms (>= 2.0.0-beta-25211-0)' for '.NETCoreApp,Version=v2.0 (win7-x64)'. [D:\Progs\C#Progs\DictPerf\DictPerf.csproj]
  Lock file has not changed. Skipping lock file write. Path: D:\Progs\C#Progs\DictPerf\obj\project.assets.json
  Restore failed in 127.88 ms for D:\Progs\C#Progs\DictPerf\DictPerf.csproj.

  Errors in D:\Progs\C#Progs\DictPerf\DictPerf.csproj
      Unable to resolve 'Microsoft.NETCore.Platforms (>= 2.0.0-beta-25211-0)' for '.NETCoreApp,Version=v2.0'.
      Unable to resolve 'Microsoft.NETCore.Platforms (>= 2.0.0-beta-25211-0)' for '.NETCoreApp,Version=v2.0 (win7-x64)'.

  NuGet Config files used:
      D:\Progs\C#Progs\DictPerf\NuGet.Config
      C:\Users\WinCPP\AppData\Roaming\NuGet\NuGet.Config

  Feeds used:
      M:\corefx\bin\packages\Release
      https://dotnet.myget.org/F/dotnet-core/api/v3/index.json
      https://api.nuget.org/v3/index.json

@safern
Copy link
Member

safern commented Mar 16, 2017

@WinCPP it seems like the package name/version is wrong in your csproj. Would you mind sharing the

<ItemGroup>
   <PackageReference Include="Microsoft.Private.CoreFx.NETCoreApp" ... >
</ItemGroup>

That you added on your csproj?

@WinCPP
Copy link
Author

WinCPP commented Mar 17, 2017

@safern please find below the csproj contents:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp2.0</TargetFramework>
    <RuntimeFrameworkVersion>2.0.0-beta-001776-00</RuntimeFrameworkVersion>
	<RuntimeIdentifier>win7-x64</RuntimeIdentifier>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.Private.CoreFx.NETCoreApp" Version="4.4.0-beta-25211-0" />
  </ItemGroup>
</Project>

As mentioned in the step in the doc, I made sure that the Include name matches and the version is same as in the Release folder in my build...


Update: I also tried by changing RuntimeFrameworkVersion above (2.0.0-beta-001776-00) to 2.0.0-preview1-005440 and setting RuntimeIdentifier to win10-x64, both as output by dotnet --info. The change of runtime framework version resulted into additional errors for it that weren't there previously, latter change didn't have any effect. Now the errors are,

  Errors in D:\Progs\C#Progs\DictPerf\DictPerf.csproj
      Unable to resolve 'Microsoft.NETCore.App (>= 2.0.0-preview1-005440)' for '.NETCoreApp,Version=v2.0'.
      Unable to resolve 'Microsoft.NETCore.Platforms (>= 2.0.0-beta-25211-0)' for '.NETCoreApp,Version=v2.0'.
      Unable to resolve 'Microsoft.NETCore.App (>= 2.0.0-preview1-005440)' for '.NETCoreApp,Version=v2.0 (win10-x64)'.
      Unable to resolve 'Microsoft.NETCore.Platforms (>= 2.0.0-beta-25211-0)' for '.NETCoreApp,Version=v2.0 (win10-x64)'.

@WinCPP
Copy link
Author

WinCPP commented Mar 17, 2017

Going by the error string that I'm getting,

      Unable to resolve 'Microsoft.NETCore.Platforms (>= 2.0.0-beta-25211-0)' for '.NETCoreApp,Version=v2.0'.

does it mean that I have to build by local CoreClr and / or CoreFx with .NETCoreApp version 2.0? And then only I can use my local bits...? Because following is the output for dotnet --info command in the git shell for my local corefx repo.

M:\corefx [issue-15622 ≡ +0 ~3 -0 !]> dotnet --info
.NET Command Line Tools (1.0.0-preview2-003131)

Product Information:
 Version:            1.0.0-preview2-003131
 Commit SHA-1 hash:  635cf40e58

Runtime Environment:
 OS Name:     Windows
 OS Version:  10.0.10586
 OS Platform: Windows
 RID:         win10-x64

@karelz
Copy link
Member

karelz commented Mar 17, 2017

Note that ".NET Command Line Tools" version != .NET Core version. The tools can target multiple .NET Core versions (like VS).

Here's example of .NET Core only API addition, if that helps: https://github.com/dotnet/corefx/issues/1942
[updated link to a better one]

@WinCPP
Copy link
Author

WinCPP commented Mar 17, 2017

@karelz The link discusses about performance, but I could not get the fix for my issue...

Actually there are two local builds to be used CoreClr and CoreFx and now I notice that each of them has user your build / dogfooding documents here and here respectively, with former pointing to latter.

Steps in the former document for using local CoreClr bits worked perfectly. However this being a case of adding new API with implementation in CoreClr and facade in CoreFx, I thought I have to do the steps in CoreFx dogfooding document as well; and there it fails. So are the steps for using local CoreFx not to be done and, just do steps for local CoreClr? Then, how would I get the new API on CoreFx facade?

@safern
Copy link
Member

safern commented Mar 17, 2017

adding @joperezr as he might certainly know the answer to help you @WinCPP.

@WinCPP
Copy link
Author

WinCPP commented Mar 17, 2017

I was still not able to solve the above problems so I abandoned that approach. I made copy of System.Collections.Tests and modified its contents and added performance tests for only two implementations of Remove:

API implementation
OLD - bool Remove(TKey key)
NEW - bool Remove(Tkey key) that internally uses Remove(TKey key, out value)

Following is the performance data that I got. Please let me know if it makes sense.

For a dictionary of int, int
Number of iterations: 20
Number of entries processed in each iteration: 2000000
Data points: timings in milliseconds
            OLD             NEW
           19.00           22.00
           19.00           21.00
           20.00           21.00
           19.00           21.00
           19.00           21.00
           19.00           21.00
           19.00           21.00
           19.00           21.00
           19.00           21.00
           19.00           24.00
           19.00           21.00
           19.00           21.00
           19.00           22.00
           19.00           21.00
           19.00           21.00
           19.00           21.00
           19.00           21.00
           19.00           21.00
           19.00           21.00
           19.00           21.00
Average:   19.05           21.25
Std Dev:    0.217944947     0.698212002


Dictionary of int, struct of 8 ints
Number of iterations: 20
Number of entries processed in each iteration: 2000000
Data points: timings in milliseconds
            OLD            NEW
           23.00          25.00
           22.00          24.00
           22.00          24.00
           22.00          24.00
           22.00          24.00
           23.00          24.00
           22.00          24.00
           22.00          24.00
           22.00          24.00
           23.00          24.00
           22.00          27.00
           22.00          24.00
           22.00          24.00
           22.00          24.00
           22.00          24.00
           22.00          24.00
           22.00          24.00
           22.00          24.00
           22.00          24.00
           22.00          25.00
Average:   22.15          24.25
Std Dev:    0.357071421    0.698212002

@WinCPP
Copy link
Author

WinCPP commented Mar 17, 2017

@danmosemsft @mikedn @karelz @safern Please review the above data.

@danmoseley
Copy link
Member

Thanks @WinCPP . This is a very hot method, so we don't want to take a 10% performance regression. I would try this approach now

pass a bool parameter to the method indicating whether to perform the copy or not. That would imply moving all this code to a new private method and having both Remove overloads call that method.

Unless @jkotas has another idea.

@jkotas
Copy link
Member

jkotas commented Mar 18, 2017

Unless @jkotas has another idea.

Well, the Dictionary is big and fat type already and each extra convenience APIs is making it fatter. The lookup loop is replicated in the implementation many times, so duplicating it one more time is hardly going to register on the radar. If we wanted to avoid this duplication, I think that the naïve implementation would be the best way to do it:

if(!TryGetValue(key, out value)) return false;
Remove(key);
return true;

Yes, it is not as fast as possible, but I do not see a problem with it. Dictionary is not the fastest collection for any given case. It is a collection that is fast enough for most common uses.

The performance effects of adding every convenience API imaginable over time to the common collections is something for fxdc to think about. I am not sure whether this particular API would made it if it was my call... .

@karelz
Copy link
Member

karelz commented Mar 18, 2017

@jkotas what kind of performance impact do you see in adding more convenience APIs? Size of the type? Code locality?
If we accept that convenience APIs don't need to be super-performant, do you see any other problem with the simple solution you outlined above?

@WinCPP
Copy link
Author

WinCPP commented Mar 18, 2017

pass a bool parameter to the method indicating whether to perform the copy or not. That would imply moving all this code to a new private method and having both Remove overloads call that method.

Unless @jkotas has another idea.

@danmosemsft There is an out parameter that will have to be mandatorily assigned before leaving the private method. Essentially something like this,

// Following is the core for the private method
if (FoundInDictionary)
{
    if (WantTheValueOut)
    {
        value = entries[i].value;
    }
    else
    {
        value = default(TValue); // out parameter hence all return paths should assign a value
    }
    // Do the actual remove stuff
    //
    return true;
}

value = default(TValue);
return false;

Therefore, a copy or a create is inevitable and will penalize the old Remove(TKey key) implementation invariably. So I am not sure if branching is going to help, unless it were a ref parameter instead of out. Appreciate your thoughts. Do we still want to collect stats for this?

I will capture stats for Remove(TKey key, out value) as a wrapper around TryGetValue and Remove(TKey key) as suggested by @jkotas ... The issue mentions this as a performance problem as double look ups happen on the dictionary for same key, once for fetching value an then for removal. Perhaps the data might help us...

Just a layman's wishful thinking... I always wish if inlining for compilers could be smart to detect plain vanilla function overloads in which one wraps the other (Remove(TKey key) and Remove(TKey key, out TValue value) pair or the chained constructors)... In such case the expression tree for the inner call could be duplicated into the outer wrapper during intermediate code generation and then aggressively optimized... regardless of whether it is concrete or generic type.

@jkotas
Copy link
Member

jkotas commented Mar 18, 2017

@jkotas what kind of performance impact do you see in adding more convenience APIs? Size of the type?

Yes, size of the type - that gets multipled for generic types. Contributes to disk footprint, startup time, ... .

If we accept that convenience APIs don't need to be super-performant

Then they can be extension methods against the interface... .

@mikedn
Copy link

mikedn commented Mar 18, 2017

Just a layman's wishful thinking... I always wish if inlining for compilers could be smart to detect plain vanilla function overloads in which one wraps the other (Remove(TKey key) and Remove(TKey key, out TValue value) pair or the chained constructors)...

What does that solve? You still end up with a bunch of additional native code being generated.

To add some numbers to this story: the existing Remove method has 300-500 bytes depending on the involved key and value types (e.g. Dictionary<int, byte>.Remove(int) has ~400 bytes). Every additional unshared instantiation adds another 300-500 bytes. Duplicating the Remove code in the new overload doubles that amount.

@mikedn
Copy link

mikedn commented Mar 18, 2017

if(!TryGetValue(key, out value)) return false;
Remove(key);
return true;

Maybe we can build both Remove overloads on top of FindEntry? It's been a while since I starred at the dictionary code but it seems that there's no reason to have multiple lookups except to save a range check or two.

@WinCPP
Copy link
Author

WinCPP commented Mar 18, 2017

Maybe we can build both Remove overloads on top of FindEntry?

Current find entry returns the index for the found entry by abstracting the direct bucket / entry list. Remove APIs work at the found location to adjust the root / next links for the buckets / entries to effect a removal; essentially this removal code is again a common block which is in fact dependent on results of the FindEntry functional block. It is between these two common functional blocks that the second Remove overload saves off the value for the entry that was located by FindEntry block and is about to be cleaned up by the removal block...

Hope I am making sense...

ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key);
}

if (buckets != null)
Copy link
Member

Choose a reason for hiding this comment

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

We don't need to duplicate this for loop code, we have a private method called FindEntry which returns the index to that entry and if it is not found it returns -1 but it won't update the last value so we could probably add a FindEntryRemove(key) where it finds the entry and does the bucket/entries update and then return the value... something like:

private int FindEntryRemove(TKey key)
{
       if (buckets != null)
       {
           int hashCode = comparer.GetHashCode(key) & 0x7FFFFFFF;
           int bucket = hashCode % buckets.Length;
           int last = -1;
           for (int i = buckets[bucket]; i >= 0; last = i, i = entries[i].next)
           {
               if (entries[i].hashCode == hashCode && comparer.Equals(entries[i].key, key))
               {
                    if (last < 0)
                   {
                        buckets[bucket] = entries[i].next;
                   }
                   else
                   {
                        entries[last].next = entries[i].next;
                   }
                   return i;
               }
            }
       }
       return -1;
}

And then both removes could be implemented around that private API, something like:

int i = FindEntryRemove(key);
if (i > 0)
{
    value = entries[i].value;
    FreeEntry(i);
    return true;
}
value = default(TValue);
return false;

And we could add a private method called FreeEntry(int index), that basically does:

entries[i].hashCode = -1;
entries[i].next = freeList;
entries[i].key = default(TKey);
entries[i].value = default(TValue);
freeList = i;
freeCount++;
version++;

@jkotas @danmosemsft any thoughts?

Copy link
Member

@jkotas jkotas Apr 10, 2017

Choose a reason for hiding this comment

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

I am not sure whether it would be a win. You would need to collect throughput and size data to tell what is better. I am ok with the implementation in one big method as is.

I would rather look into how to make the native code generated for this implementation smaller. For example, caching pointer to entries[i] in a local may help to reduce native code size speed and improve speed: ref Entry entry = ref entries[i];

Copy link
Author

Choose a reason for hiding this comment

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

@safern I believe this is in the pursuit of reducing duplicated code. But then the duplication, although restricted to few lines, happens between FindEntry and FindEntryAndRemove for the finding part. Unless we want to merge them and pass a boolean to indicate 'remove' that will default to false...? ... potentially introducing a branch on the paths where existing FindEntry is used. Either ways, the method FindEntryAndRemove still feels incomplete from design point of view because we have removed but not free'd the entry and freeing part comes as post fix...

@jkotas

  1. Curiosity question, wouldn't the C# compiler perform local CSE on the four lines which fetch the entry ... i.e. entry[i].* lines... with effect being that the entry is computed only once? This is based on my experience with C / C++ compilers though...
  2. How does the usage of default(TValue) work? Does it initialize and then do a memberwise copy to destination (due to operator =) or, does it directly construct the default into memory referenced by the destination? If it is latter, does it still do memberwise init or it does sort of memset to zeros using span APIs? Thought in my head: cycles spent in memberwise init to default for a value type will be proportional to its size.

Copy link
Member

Choose a reason for hiding this comment

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

C# compiler does very few trivial optimizations only. Optimizations like CSE are done by the JIT. Whether or not the optimization kicks in depends on number of factors. The best way to know for sure is to look the disassembly of the generated code and measure.

One thing to keep in mind is that the array indexing in C# is bound-checked. It restricts the optimizations that the JIT can do transparently, for example it cannot do the bound-check speculatively because of it would result in spurious exceptions.

I have glanced over the native code generated for Dictionary<int,int>.Remove. The CSE for entry[i] kicks in some cases, but not others. If you cache ref Entry entry = ref entries[i]; at the start of the loop and replace all 9 occurrences of entries[i] with it, the native code generated for this method should be approximately 20% smaller.

Copy link
Member

Choose a reason for hiding this comment

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

Either way, I think this one is fine to merge as is, and the optimizations for both the new and existing methods should be done in separate PR.

Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Author

Choose a reason for hiding this comment

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

@jkotas thanks for the elaborate answer. I helps me understand more internals. I had not given a thought that the operator [] would do bounds checking... took it for plain simple C / C++ array access.

{
if (key == null)
{
value = default(TValue);
Copy link
Member

Choose a reason for hiding this comment

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

What is the point of setting the value on exception thrown? It is not how similar method works. For example, Dictionary.TryGetValue won't set the value on exception.

Copy link
Author

Choose a reason for hiding this comment

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

@jkotas hmmm as I mentioned earlier, in the initial code I did leave out assigning default to the out parameter referring to some method but I am not able to find out which. I did the latest change based on Dictionary.TryGetValue itself which actually assigned default value on false path (and in fact there is no exception since it is a Try method)... please refer here.

So looked up MSDN for what should be the behavior of an out parameter in C# (link) and it says this...

Although variables passed as out arguments do not have to be initialized before being passed, the called method is required to assign a value before the method returns.

And then this aligned with the TryGetValue behavior mentioned previously and also with COM which mentions that callee is supposed to assign value to out parameter even on E_FAIL, in which case it may be a NULL pointer... hence...

Copy link
Member

Choose a reason for hiding this comment

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

Although variables passed as out arguments do not have to be initialized before being passed, the called method is required to assign a value before the method returns.

This is only applicable when the method returns to its caller; if it instead throws an exception, this does not apply.

Copy link
Author

Choose a reason for hiding this comment

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

@stephentoub Thanks for the clarification. Removing.

@jkotas
Copy link
Member

jkotas commented Apr 11, 2017

did the latest change based on Dictionary.TryGetValue itself which actually assigned default value on false path

Assigning on the false path is fine. I am questioning assigning on the exception throwing path. Dictionary.TryGetValue does not assign on exception throwing path either.

@WinCPP
Copy link
Author

WinCPP commented Apr 11, 2017

@jkotas ... yup got it. Removing. Now I recollect, the exception was inside FindEntry based on which I had initially actually not included default assignment on exception path... but then I forgot how I had reached to that conclusion...

@WinCPP
Copy link
Author

WinCPP commented Apr 11, 2017

Hmm... then as for the behavior of the API, can we say that the value in the out parameter will remain unaltered after the Remove(K, out V) if the API throws a null key exception? I could not find test case for Dictionary.TryGetValue where null key is passed... so wondering what should be the expected behavior. Or do we want to say behavior not defined...? I have one test case for the new overload for null key exception and I will modify that accordingly.

Between... just curiosity.... the other Try* methods that I have used ... Enum.TryParse, Int32.TryParse do not throw exceptions which is what I thought is the reason for 'Try*` methods... i.e. eliminate the exception handling blocks... I do appreciate that a null key is different thing altogether, but then the user said 'try' and the API tried, but there is nothing in there...

@jkotas
Copy link
Member

jkotas commented Apr 11, 2017

Try* does not apply to argument validation. If you pass invalid argument to the API, it will throw. TryParse methods are not different - for example, if you pass bad NumberStyles to int.TryParse, you will get an exception there as well.

@stephentoub
Copy link
Member

then as for the behavior of the API, can we say that the value in the out parameter will remain unaltered after the Remove(K, out V) if the API throws a null key exception?

The value is undefined in such situations, but ideally in most situations if the target previously had a value assigned it would remain unchanged. From a language perspective, out is only considered an assignment for definite assignment purposes if the call returns rather than throws.

@WinCPP
Copy link
Author

WinCPP commented Apr 11, 2017

Thanks @jkotas @stephentoub . I'll update the test case to expect that the contents of the out parameter remain unaltered...

@WinCPP
Copy link
Author

WinCPP commented Apr 11, 2017

@safern @jkotas @stephentoub Please review the latest commit. Thanks.

@@ -628,6 +628,48 @@ public bool Remove(TKey key)
return false;
}

public bool Remove(TKey key, out TValue value)
Copy link
Member

Choose a reason for hiding this comment

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

Maybe a comment explaining that we're duplicating the code of Remove(TKey) for performance reasons. Othrwise someone will want to refactor it later...

Copy link
Author

Choose a reason for hiding this comment

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

Hmmm... But then @jkotas has opened #10883 ... would that cover this as well...?

Copy link
Member

Choose a reason for hiding this comment

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

#10883 is not about eliminating the code duplication between the different overloads.

Copy link
Author

Choose a reason for hiding this comment

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

Oops ok. @danmosemsft @jkotas I have added a comment above both overloads. Kindly review the same.

Copy link
Member

@danmoseley danmoseley left a comment

Choose a reason for hiding this comment

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

LGTM but Jan should sign off also.

@danmoseley
Copy link
Member

Looks good thanks for doing this @WinCPP !

Nit: if you'd made the comment addition in a 2nd commit, rather than squashing locally, we could easily see it was the only change you make. When we merge, there is a squash button so there's no need to squash everything yourself.

@danmoseley
Copy link
Member

I'll merge when CI's green.

@danmoseley
Copy link
Member

@dotnet-bot test tizen armel cross debug build (https://github.com/dotnet/coreclr/issues/10897)

@danmoseley
Copy link
Member

@dotnet-bot test tizen armel cross release build (https://github.com/dotnet/coreclr/issues/10897)

@WinCPP
Copy link
Author

WinCPP commented Apr 11, 2017

@danmosemsft Oops ok. Will keep that in mind next time onwards.

@danmoseley
Copy link
Member

Ignoring Tizen as per #10897

@danmoseley danmoseley merged commit 5748039 into dotnet:master Apr 11, 2017
@omariom
Copy link

omariom commented May 10, 2017

@jkotas

Yes, size of the type - that gets multipled for generic types.

Even when the method doesn't use any of the generic params? Like return 1;

@jkotas
Copy link
Member

jkotas commented May 10, 2017

Yes, it is the case today.

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.