【问题标题】:C# Compiler Optimising Loop?C#编译器优化循环?
【发布时间】:2014-11-21 22:33:55
【问题描述】:

我正在尝试控制对对象的访问,以便在给定的时间跨度内只能访问一定次数。在我拥有的一个单元测试中,访问限制为每秒一次。所以 5 次访问应该只需要 4 秒多一点。但是,我们的 TFS 服务器上的测试失败了,只用了 2 秒。我的代码的精简版本在这里:

public class RateLimitedSessionStrippedDown<T>
{
    private readonly int _rateLimit;
    private readonly TimeSpan _rateLimitSpan;
    private readonly T _instance;
    private readonly object _lock;

    private DateTime _lastReset;
    private DateTime _lastUse;
    private int _retrievalsSinceLastReset;

    public RateLimitedSessionStrippedDown(int limitAmount, TimeSpan limitSpan, T instance )
    {
        _rateLimit = limitAmount;
        _rateLimitSpan = limitSpan;
        _lastUse = DateTime.UtcNow;
        _instance = instance;
        _lock = new object();
    }

    private void IncreaseRetrievalCount()
    {
        _retrievalsSinceLastReset++;
    }

    public T GetRateLimitedSession()
    {
        lock (_lock)
        {
            _lastUse = DateTime.UtcNow;

            Block();

            IncreaseRetrievalCount();

            return _instance;
        }
    }

    private void Block()
    {
        while (_retrievalsSinceLastReset >= _rateLimit &&
           _lastReset.Add(_rateLimitSpan) > DateTime.UtcNow)
        {
            Thread.Sleep(TimeSpan.FromMilliseconds(10));
        }

        if (DateTime.UtcNow > _lastReset.Add(_rateLimitSpan))
        {
            _lastReset = DateTime.UtcNow;
            _retrievalsSinceLastReset = 0;
        }
    }
}

在我的计算机上运行时,无论是在调试还是发布中,它都可以正常工作。但是,一旦我提交到我们的 TFS 构建服务器,我的单元测试就会失败。这是测试:

    [Test]
    public void TestRateLimitOnePerSecond_AssertTakesAtLeastNMinusOneSeconds()
    {
        var rateLimiter = new RateLimitedSessionStrippedDown<object>(1, TimeSpan.FromSeconds(1), new object());

        DateTime start = DateTime.UtcNow;

        for (int i = 0; i < 5; i++)
        {
            rateLimiter.GetRateLimitedSession();
        }

        DateTime end = DateTime.UtcNow;

        Assert.GreaterOrEqual(end.Subtract(start), TimeSpan.FromSeconds(4));
    }

我想知道测试中的循环是否正在以某种方式进行优化,即它在单独的线程(或类似的东西)上运行循环的每次迭代,这意味着测试完成得比它应该更快,因为只有 Thread.Sleep阻塞正在调用它的线程?

【问题讨论】:

  • 代码有一些奇怪的地方——比如,我不明白为什么你有_lastUse,最好使用Stopwatch而不是DateTime.UtcNow来跟踪时间 - 但我没有看到任何可以解释问题的明显错误。唯一想到的是(因为您没有使用Stopwatch),如果系统时间由于某种原因未能更新,时钟时间将错误地测量实际经过的时间。这极不太可能与编译器优化有关,尤其是涉及线程的优化。
  • 问问自己当_lastReset.Add(_rateLimitSpan) == DateTime.UtcNow 时会发生什么。此外,对于这个写得很好的代码,依赖_lastReset = default(DateTime) 初始化是奇怪和不透明的。此外,真正的代码不应该Sleep()。考虑等待 Waithandle 或其他东西。
  • 此测试失败的一种不太可能但理论上绝对可能的方式是,如果在您运行测试时与时间服务器同步(并且您无法控制)。不过,如果这确实是问题所在,我会感到非常惊讶。
  • 发生了一件非常奇怪的事情——我对同一个项目的另一部分进行了更改,提交了代码,并且所有测试都通过了。

标签: c# multithreading loops compiler-optimization


【解决方案1】:

您的问题在 Block 方法内部,现在我查看了 cmets,看来 Henk Holterman 已经提出了这个问题。

只有当_lastReset.Add(_rateLimitSpan) 和DateTime.UtcNow 相等时才会失败。这不会经常发生,因此它会间歇性地失败。解决方法是在这一行将 &gt; 更改为 &gt;=:

if (DateTime.UtcNow > _lastReset.Add(_rateLimitSpan))

这并不直观,除非您明白每次调用 DateTime.UtcNow 不一定每次调用都返回一个新值。

尽管DateTime.UtcNow 的精度高达100 纳秒,但其精度与精度并不相同。它依赖于机器的计时器间隔,范围为 1-15 毫秒,但通常设置为 15.25 毫秒,除非您正在使用多媒体。

您可以通过dotnetfiddle 看到这一点。除非您打开了将计时器设置为不同值(例如 1 毫秒)的程序,否则您会注意到滴答之间的差异约为 150000 滴答,大约 15 毫秒,或正常的系统计时器间隔。

我们还可以通过将对DateTime.UtcNow 的调用提升到临时变量中并在方法结束时比较它们来验证这一点:

    private void Block()
    {
        var first = DateTime.UtcNow;
        while (_retrievalsSinceLastReset >= _rateLimit &&
           _lastReset.Add(_rateLimitSpan) > first)
        {
            Thread.Sleep(TimeSpan.FromMilliseconds(10));
            first = DateTime.UtcNow;
        }

        var second = DateTime.UtcNow;
        if (second > _lastReset.Add(_rateLimitSpan))
        {
            _lastReset = DateTime.UtcNow;
            _retrievalsSinceLastReset = 0;
        }

        if (first == second)
        {
            Console.WriteLine("DateTime.UtcNow returned same value");
        }
    }

在我的机器上,对Block 的所有五个调用都打印出DateTime.UtcNow 相等。

【讨论】:

    猜你喜欢
    • 2012-12-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-02-21
    • 1970-01-01
    • 2015-09-30
    相关资源
    最近更新 更多