优化读取事务启动时间

提醒一下,这就是我们的出发点:

优化读取事务启动时间

这是我们在最后一篇文章中停下来的地方:

优化读取事务启动时间
sqlserver数据库同步

现在,我可以看到我们花了相当多的时间在HashSet的AddifNotPresent方法上。 因为我们之前删除了对只写事务状态的任何调用,这意味着我们在事务中有一些使用哈希集的东西,在这个场景中,只向其中添加了一个项目。 检查代码显示这是页面状态变量。

事务需要保存寻呼机状态,这样它们可以确保寻呼机知道事务何时开始和结束。 为此,我们在适当的时候调用AddRef/Release。 好的一面是我们实际上没有 在乎 如果我们多次持有同一个页面状态。 只要我们调用相同数量的AddRef/Release,我们就很好。 因此,我们可以放弃HashSet,取而代之的是一个常规列表,它为我们提供:

优化读取事务启动时间

这大约是我们在这个基准测试中节省的一秒半时间。但是请注意,我们仍然在列表上花费了相当多的时间。添加方法。 深入观察,我们可以看到所有这些时间都花在这里:

优化读取事务启动时间

所以第一个Add()需要分配,这很昂贵。

我决定用两种不同的方法来解决这个问题。 第一个是定义初始容量为2,这应该足以覆盖大多数常见的场景。 这导致了以下结果:

优化读取事务启动时间

因此,预先指定容量对我们的性能有很大的影响,又降低了整整一秒钟。 我决定尝试的下一件事是看看链表是否会更好。 这通常是非常小的,不管怎样,我们对它进行的唯一迭代是在处理期间(通常只有一两个这样的迭代)。

也就是说,我不确定当我们预先指定了大小时,我们是否能击败列表性能。 链接列表。毕竟,添加()需要分配和一个列表。添加只是设置一个值。

优化读取事务启动时间

所以…不,我们不会使用这个优化。

现在,让我们回到这个场景中真正的重量级人物。 GetPageStateAll抓痕和GetSnapshots。 它们加起来占了这个场景总成本的36%,这实在是太贵了。 在这里,我们可以利用我们对代码的了解,并认识到这些值只能通过写事务来更改,并且永远不会更改。 这给了我们一个很好的机会来做一些缓存。

当我们将创建所有划痕的页导航状态的责任转移到写事务时,情况如下:

优化读取事务启动时间

现在让我们对GetSnapShots()做同样的事情,它给我们提供了:

优化读取事务启动时间

提醒一下,低级别交易。ctor 开始 36岁。3秒,现在我们讨论的是6秒。6。 因此,我们将性能成本降低了82%以上。

一次这样的呼叫的成本降低到7微秒 在侧写下.

也就是说,OpenReadTransaction的成本从48开始。1秒,我们把它降到了17秒。6秒。 因此,我们的成本降低了63%,但看起来我们现在有比低级别事务构造器更有趣的东西要看……

优化读取事务启动时间

首先要注意的是,确保页面状态引用最终调用_页面状态。添加()时,它会遇到同样的成本问题,因为它需要增加容量。

优化读取事务启动时间

增加初始容量带来了可观的收益。

优化读取事务启动时间

这样,我们就可以继续分析其余的成本。 我们可以看到,并发字典上的尝试添加非常昂贵*。

*对于给定值 事实上

它只需要不到3微秒就能完成,但这仍然是我们能做的很大一部分。

我们需要这个调用的原因是我们需要跟踪活动的事务。 这样做是因为我们需要知道谁是MVCC最早运行的事务。 最简单的方法是将它放入并发字典中,但是对于这类工作负载来说,这是很昂贵的。 我已经用一个专用的类来切换它,这允许我们围绕它做更好的优化。

我们最终采用的设计有点复杂(在profiler输出之后会更复杂),但它给了我们这样的结果:

优化读取事务启动时间

因此,我们的成本只是并行词典成本的三分之一。 我们在每个线程中使用了一个专用数组,所以没有争用。 问题是我们不能这样做,我们需要读取所有这些值,我们可能会从不同的线程关闭一个事务。 正因为如此,我们把逻辑分开了。 每个包含包装类的线程都有一个数组,我们给事务提供对包装类实例的访问。 因此,当它被释放时,它将清除包装类中的值。

然后,一旦内存写入到达原始线程,我们就可以在原始线程中重用该实例。 在此之前,我们只会对该值进行陈旧的读取并忽略它。 它更复杂,需要花一点时间才能做好,但性能证明了这一点。

目前的情况是我们从48岁开始。这个基准测试只有1秒钟,现在是14秒钟。OpenReadTransaction为7秒。 那是一天的好工作。