-
-
Notifications
You must be signed in to change notification settings - Fork 3.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
ILSpy produces code with a redundant return statement not present in the CIL code #2924
Comments
This is the code responsible for the behavior: ILSpy/ICSharpCode.Decompiler/IL/ControlFlow/ControlFlowSimplification.cs Lines 172 to 182 in de8f133
I haven't looked into it, but it might be possible to make it less aggressive by checking whether there is any "useful" code after the end of the finally block other than a return. Further analysis reveals that early in the decompiler pipeline the code would have been decompiled using an early return, which would not look as strange as the code above does... however, then the ReduceNestingTransform kicks in and actually increases the nesting by moving everything into the if-statement. |
Another option would be to extend the "common exit point" detection to detect both returns as equivalent exit points, and which will cause In general, there is no one to one correspondence between IL |
I did some further testing using Decompiler change applied: ILSpy/ICSharpCode.Decompiler/IL/ControlFlow/ControlFlowSimplification.cs Lines 170 to 183 in caec6a6
The general conclusion is that the current Here are two examples of the original source code compared against the two decompiler versions tested: Example: SemaphoreSlim.Release(int)Original source code: public int Release(int releaseCount)
{
CheckDispose();
if (releaseCount < 1)
{
throw new ArgumentOutOfRangeException(
nameof(releaseCount), releaseCount, SR.SemaphoreSlim_Release_CountWrong);
}
int returnCount;
lock (m_lockObjAndDisposed)
{
// Read the m_currentCount into a local variable to avoid unnecessary volatile accesses inside the lock.
int currentCount = m_currentCount;
returnCount = currentCount;
// If the release count would result exceeding the maximum count, throw SemaphoreFullException.
if (m_maxCount - currentCount < releaseCount)
{
throw new SemaphoreFullException();
}
// Increment the count by the actual release count
currentCount += releaseCount;
// Signal to any synchronous waiters, taking into account how many waiters have previously been pulsed to wake
// but have not yet woken
int waitCount = m_waitCount;
Debug.Assert(m_countOfWaitersPulsedToWake <= waitCount);
int waitersToNotify = Math.Min(currentCount, waitCount) - m_countOfWaitersPulsedToWake;
if (waitersToNotify > 0)
{
// Ideally, limiting to a maximum of releaseCount would not be necessary and could be an assert instead, but
// since WaitUntilCountOrTimeout() does not have enough information to tell whether a woken thread was
// pulsed, it's possible for m_countOfWaitersPulsedToWake to be less than the number of threads that have
// actually been pulsed to wake.
if (waitersToNotify > releaseCount)
{
waitersToNotify = releaseCount;
}
m_countOfWaitersPulsedToWake += waitersToNotify;
for (int i = 0; i < waitersToNotify; i++)
{
Monitor.Pulse(m_lockObjAndDisposed);
}
}
// Now signal to any asynchronous waiters, if there are any. While we've already
// signaled the synchronous waiters, we still hold the lock, and thus
// they won't have had an opportunity to acquire this yet. So, when releasing
// asynchronous waiters, we assume that all synchronous waiters will eventually
// acquire the semaphore. That could be a faulty assumption if those synchronous
// waits are canceled, but the wait code path will handle that.
if (m_asyncHead is not null)
{
Debug.Assert(m_asyncTail is not null, "tail should not be null if head isn't null");
int maxAsyncToRelease = currentCount - waitCount;
while (maxAsyncToRelease > 0 && m_asyncHead is not null)
{
--currentCount;
--maxAsyncToRelease;
// Get the next async waiter to release and queue it to be completed
TaskNode waiterTask = m_asyncHead;
RemoveAsyncWaiter(waiterTask); // ensures waiterTask.Next/Prev are null
waiterTask.TrySetResult(result: true);
}
}
m_currentCount = currentCount;
// Exposing wait handle if it is not null
if (m_waitHandle is not null && returnCount == 0 && currentCount > 0)
{
m_waitHandle.Set();
}
}
// And return the count
return returnCount;
} The original decompiler output: public int Release(int releaseCount)
{
CheckDispose();
if (releaseCount < 1)
{
throw new ArgumentOutOfRangeException("releaseCount", releaseCount, SR.SemaphoreSlim_Release_CountWrong);
}
lock (m_lockObjAndDisposed)
{
int currentCount = m_currentCount;
int num = currentCount;
if (m_maxCount - currentCount < releaseCount)
{
throw new SemaphoreFullException();
}
currentCount += releaseCount;
int waitCount = m_waitCount;
int num2 = Math.Min(currentCount, waitCount) - m_countOfWaitersPulsedToWake;
if (num2 > 0)
{
if (num2 > releaseCount)
{
num2 = releaseCount;
}
m_countOfWaitersPulsedToWake += num2;
for (int i = 0; i < num2; i++)
{
Monitor.Pulse(m_lockObjAndDisposed);
}
}
if (m_asyncHead != null)
{
int num3 = currentCount - waitCount;
while (num3 > 0 && m_asyncHead != null)
{
currentCount--;
num3--;
TaskNode asyncHead = m_asyncHead;
RemoveAsyncWaiter(asyncHead);
asyncHead.TrySetResult(true);
}
}
m_currentCount = currentCount;
if (m_waitHandle != null)
{
if (num == 0)
{
if (currentCount > 0)
{
m_waitHandle.Set();
return num;
}
return num;
}
return num;
}
return num;
}
} The modified decompiler output: public int Release(int releaseCount)
{
CheckDispose();
if (releaseCount < 1)
{
throw new ArgumentOutOfRangeException("releaseCount", releaseCount, SR.SemaphoreSlim_Release_CountWrong);
}
int num;
lock (m_lockObjAndDisposed)
{
int currentCount = m_currentCount;
num = currentCount;
if (m_maxCount - currentCount < releaseCount)
{
throw new SemaphoreFullException();
}
currentCount += releaseCount;
int waitCount = m_waitCount;
int num2 = Math.Min(currentCount, waitCount) - m_countOfWaitersPulsedToWake;
if (num2 > 0)
{
if (num2 > releaseCount)
{
num2 = releaseCount;
}
m_countOfWaitersPulsedToWake += num2;
for (int i = 0; i < num2; i++)
{
Monitor.Pulse(m_lockObjAndDisposed);
}
}
if (m_asyncHead != null)
{
int num3 = currentCount - waitCount;
while (num3 > 0 && m_asyncHead != null)
{
currentCount--;
num3--;
TaskNode asyncHead = m_asyncHead;
RemoveAsyncWaiter(asyncHead);
asyncHead.TrySetResult(true);
}
}
m_currentCount = currentCount;
if (m_waitHandle != null && num == 0 && currentCount > 0)
{
m_waitHandle.Set();
}
}
return num;
} Example: ResourceManager.InternalGetResourceSet(CultureInfo, bool, bool)Original source code: protected virtual ResourceSet? InternalGetResourceSet(CultureInfo culture, bool createIfNotExists, bool tryParents)
{
Debug.Assert(culture != null, "culture != null");
Debug.Assert(_resourceSets != null);
Dictionary<string, ResourceSet> localResourceSets = _resourceSets;
ResourceSet? rs = null;
CultureInfo? foundCulture = null;
lock (localResourceSets)
{
if (localResourceSets.TryGetValue(culture.Name, out rs))
{
return rs;
}
}
ResourceFallbackManager mgr = new ResourceFallbackManager(culture, _neutralResourcesCulture, tryParents);
foreach (CultureInfo currentCultureInfo in mgr)
{
lock (localResourceSets)
{
if (localResourceSets.TryGetValue(currentCultureInfo.Name, out rs))
{
// we need to update the cache if we fellback
if (culture != currentCultureInfo) foundCulture = currentCultureInfo;
break;
}
}
// InternalGetResourceSet will never be threadsafe. However, it must
// be protected against reentrancy from the SAME THREAD. (ie, calling
// GetSatelliteAssembly may send some window messages or trigger the
// Assembly load event, which could fail then call back into the
// ResourceManager). It's happened.
rs = _resourceGroveler.GrovelForResourceSet(currentCultureInfo, localResourceSets,
tryParents, createIfNotExists);
// found a ResourceSet; we're done
if (rs != null)
{
foundCulture = currentCultureInfo;
break;
}
}
if (rs != null && foundCulture != null)
{
// add entries to the cache for the cultures we have gone through
// currentCultureInfo now refers to the culture that had resources.
// update cultures starting from requested culture up to the culture
// that had resources.
foreach (CultureInfo updateCultureInfo in mgr)
{
AddResourceSet(localResourceSets, updateCultureInfo.Name, ref rs);
// stop when we've added current or reached invariant (top of chain)
if (updateCultureInfo == foundCulture)
{
break;
}
}
}
return rs;
} The original decompiler output: protected virtual ResourceSet? InternalGetResourceSet(CultureInfo culture, bool createIfNotExists, bool tryParents)
{
Dictionary<string, ResourceSet> resourceSets = _resourceSets;
ResourceSet value = null;
CultureInfo cultureInfo = null;
lock (resourceSets)
{
if (resourceSets.TryGetValue(culture.Name, out value))
{
return value;
}
}
ResourceFallbackManager resourceFallbackManager = new ResourceFallbackManager(culture, _neutralResourcesCulture, tryParents);
foreach (CultureInfo item in resourceFallbackManager)
{
lock (resourceSets)
{
if (resourceSets.TryGetValue(item.Name, out value))
{
if (culture != item)
{
cultureInfo = item;
}
break;
}
}
value = _resourceGroveler.GrovelForResourceSet(item, resourceSets, tryParents, createIfNotExists);
if (value != null)
{
cultureInfo = item;
break;
}
}
if (value != null && cultureInfo != null)
{
foreach (CultureInfo item2 in resourceFallbackManager)
{
AddResourceSet(resourceSets, item2.Name, ref value);
if (item2 == cultureInfo)
{
return value;
}
}
return value;
}
return value;
} The modified decompiler output: protected virtual ResourceSet? InternalGetResourceSet(CultureInfo culture, bool createIfNotExists, bool tryParents)
{
Dictionary<string, ResourceSet> resourceSets = _resourceSets;
ResourceSet value = null;
CultureInfo cultureInfo = null;
lock (resourceSets)
{
if (resourceSets.TryGetValue(culture.Name, out value))
{
return value;
}
}
ResourceFallbackManager resourceFallbackManager = new ResourceFallbackManager(culture, _neutralResourcesCulture, tryParents);
foreach (CultureInfo item in resourceFallbackManager)
{
lock (resourceSets)
{
if (resourceSets.TryGetValue(item.Name, out value))
{
if (culture != item)
{
cultureInfo = item;
}
break;
}
}
value = _resourceGroveler.GrovelForResourceSet(item, resourceSets, tryParents, createIfNotExists);
if (value != null)
{
cultureInfo = item;
break;
}
}
if (value != null && cultureInfo != null)
{
foreach (CultureInfo item2 in resourceFallbackManager)
{
AddResourceSet(resourceSets, item2.Name, ref value);
if (item2 == cultureInfo)
{
break;
}
}
}
return value;
} The following Overall this change looks like it positively impacts the decompiler output without any consequences. I am yet to find code that produces less accurate results with the aforementioned change applied. If this change is acceptable and the green light is given I can make a PR where I apply the change proposed above in perhaps, a more optimal way (block cloning will no longer be necessary as this patch pretty much makes this transformation equivalent to moving the block into a different parent container). Feedback on my proposed change and research is greatly appreciated! |
I'm pretty sure there used to be a reason why this code was cloning, not just moving.
would result in both I would expect that your change causes the tests to fail. Although there's some really weird interactions with the |
Input code
IL produced by compiler:
Erroneous output
There is an additional return statement in this method found inside the if block which also does not appear in the IL of the method.
The difference between foreach/for is expected due to compiler optimization
Details
The text was updated successfully, but these errors were encountered: