使用 DataTable.Add 时的性能差异极大



看看下面的程序。这是不言自明的,但无论如何我都会解释:)

我有两种方法,一种快,一种慢。这些方法执行完全相同的操作:它们创建一个包含 50,000 行和 1000 列的表。我写入表中可变数量的列。在下面的代码中,我选择了 10 (NUM_COLS_TO_WRITE_TO )。

换句话说,1000 列中只有 10 列实际包含数据。好的。这两种方法之间的唯一区别是快速填充列然后调用DataTable.AddRow,而慢速方法在之后执行此操作。就是这样。

然而,性能差异令人震惊(无论如何对我来说)。快速版本几乎完全不受我们写入的列数的影响,而慢速版本则线性上升。例如,当我写入的列数为 20 时,快速版本需要 2.8 秒,但慢速版本需要一分钟以上。

这到底是怎么回事?

我认为也许添加dt.BeginLoadData会有所作为,在某种程度上,它确实将时间从 61 秒降低到 ~50 秒,但这仍然是一个巨大的差异。

当然,显而易见的答案是,"好吧,不要那样做。好的,当然。但到底是什么原因造成的呢?这是预期行为吗?我肯定没想到。:)

public class Program
{
    private const int NUM_ROWS = 50000;
    private const int NUM_COLS_TO_WRITE_TO = 10;
    private const int NUM_COLS_TO_CREATE = 1000;
    private static void AddRowFast() {
        DataTable dt = new DataTable();            
        //add a table with 1000 columns
        for (int i = 0; i < NUM_COLS_TO_CREATE; i++) {
            dt.Columns.Add("x" + i, typeof(string));
        }
        for (int i = 0; i < NUM_ROWS; i++) {                
            var theRow = dt.NewRow();
            for (int j = 0; j < NUM_COLS_TO_WRITE_TO; j++) {
                theRow[j] = "whatever";
            }
            //add the row *after* populating it
            dt.Rows.Add(theRow);                
        }
    }
    private static void AddRowSlow() {
        DataTable dt = new DataTable();
        //add a table with 1000 columns
        for (int i = 0; i < NUM_COLS_TO_CREATE; i++) {
            dt.Columns.Add("x" + i, typeof(string));
        }
        for (int i = 0; i < NUM_ROWS; i++) {
            var theRow = dt.NewRow();
            //add the row *before* populating it
            dt.Rows.Add(theRow);
            for (int j=0; j< NUM_COLS_TO_WRITE_TO; j++){
                theRow[j] = "whatever";
            }                
        }
    }
    static void Main(string[] args)
    {
        var sw = Stopwatch.StartNew();
        AddRowFast();
        sw.Stop();
        Console.WriteLine(sw.Elapsed.TotalMilliseconds);
        sw.Restart();
        AddRowSlow();
        sw.Stop();
        Console.WriteLine(sw.Elapsed.TotalMilliseconds);
        //When NUM_COLS is 5
        //FAST: 2754.6782
        //SLOW: 15794.1378
        //When NUM_COLS is 10
        //FAST: 2777.431  ms
        //SLOW 32004.7203 ms
        //When NUM_COLS is 20
        //FAST:  2831.1733 ms
        //SLOW: 61246.2243 ms
    }
}

更新

在慢速版本中调用theRow.BeginEdittheRow.EndEdit会使慢速版本或多或少恒定(在我的机器上~4秒)。如果我真的在桌面上有一些限制,我想这对我来说可能是有意义的。

当附加到表时,需要做更多的工作来记录和跟踪每个更改的状态。

例如,如果您这样做,

theRow.BeginEdit();
for (int j = 0; j < NUM_COLS_TO_WRITE_TO; j++)
{
   theRow[j] = "whatever";
}
theRow.CancelEdit();

然后在 BeginEdit() 中,它在内部获取行内容的副本,以便您可以随时回滚 - 上述的最终结果是再次空行而没有whatever。即使在BeginLoadData模式下,这仍然是可能的。如果附加到 DataTable,则遵循 BeginEdit 的路径,最终您将进入 DataTable.NewRecord(),这表明它只是复制每列的每个值来存储原始状态,以防需要取消 - 这里没有太多魔力。另一方面,如果不附加到数据表,则BeginEdit根本没有发生太多事情,并且会很快退出。

EndEdit()同样非常重(附加时),因为这里检查了所有约束等(最大长度、列是否允许空值等)。此外,它还会触发一堆事件,明确释放用于取消编辑的存储空间,并可以使用DataTable.GetChanges()进行调用,这在BeginLoadData中仍然是可能的。事实上,查看源代码BeginLoadData似乎所做的只是关闭约束检查和索引。

因此,这描述了BeginEditEditEdit的作用,并且就存储的内容而言,它们在附加或不附加时完全不同。现在考虑一下,你可以在 DataRow 的索引器资源库上看到单个theRow[j] = "whatever"它调用BeginEditInternal,然后在每次调用时EditEdit(如果它尚未在编辑中,因为你之前显式调用了BeginEdit)。因此,这意味着每次执行此调用时,它都会复制并存储行中每一列的每个值。所以你这样做了 10 次,这意味着对于你的 1,000 列 DataTable,超过 50,000 行,这意味着它分配了 500,000,000 个对象。最重要的是,每次更改后都会触发所有其他版本控制、检查和事件,因此,总体而言,将行附加到 DataTable 时比未附加到 DataTable 时慢得多。

最新更新