使用 DataTable.Add 时的极端性能差异
Extreme performance difference when using DataTable.Add
看看下面的程序。这是不言自明的,但无论如何我都会解释:)
我有两种方法,一种快,一种慢。这些方法做完全相同的事情:它们创建一个具有 50,000 行和 1000 列的 table。我在 table 中写入可变数量的列。在下面的代码中,我选择了 10 (NUM_COLS_TO_WRITE_TO
).
换句话说,1000 列中只有 10 列实际包含数据。好的。这两种方法的 only 区别在于快速填充列,然后 then 调用 DataTable.AddRow
,而慢速填充列 then,而慢速填充列之后。就是这样。
然而,性能差异令人震惊(无论如何对我来说)。快速版本几乎完全不受更改我们写入的列数的影响,而慢速版本则呈线性上升。例如,当我写入的列数为 20 时,快速版本需要 2.8 秒,但慢速版本需要 分钟。
这里到底发生了什么?
我想也许添加 dt.BeginLoadData
会有所不同,并且在某种程度上确实如此,它使时间从 61 秒减少到大约 50 秒,但这仍然是一个 巨大的 区别。
当然,显而易见的答案是,"Well, don't do it that way."好的。当然。但是到底是什么原因造成的呢?这是预期的行为吗?我当然没想到。 :)
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.BeginEdit
和 theRow.EndEdit
会使慢速版本或多或少保持不变(在我的机器上约为 4 秒)。如果我实际上 对 table 有一些限制,我想这对我来说可能有意义。
附加到 table 后,需要做更多的工作来记录和跟踪每次更改的状态。
例如,如果你这样做,
theRow.BeginEdit();
for (int j = 0; j < NUM_COLS_TO_WRITE_TO; j++)
{
theRow[j] = "whatever";
}
theRow.CancelEdit();
然后在 BeginEdit()
中,internally it's taking a copy of the contents of the row, so that at any point, you can rollback - and the end result of the above is an empty row again without whatever
. This is still possible, even when in BeginLoadData
mode. Following the path ofBeginEdit
if attached to a DataTable, eventually you get into DataTable.NewRecord() 表明它只是复制每一列的每个值以存储原始状态,以防需要取消 - 这里没有太多魔法。另一方面,如果没有附加到数据 table,BeginEdit
中根本不会发生什么,它会很快退出。
EndEdit()
同样很重(附加时),因为这里检查了所有约束等(最大长度、列是否允许空值等)。它还会触发一系列事件,明确释放在编辑被取消的情况下使用的存储空间,并允许使用 DataTable.GetChanges()
进行召回,这在 BeginLoadData
中仍然是可能的。事实上,查看源代码 BeginLoadData
似乎所做的就是关闭约束检查和索引。
所以这描述了 BeginEdit
和 EditEdit
的作用,并且它们在附加或不附加时在存储的内容方面完全不同。现在考虑单个 theRow[j] = "whatever"
您可以在索引器 setter 上看到 DataRow 它调用 BeginEditInternal
然后在每次调用时调用 EditEdit
(如果不是已经在编辑中,因为您之前明确调用了 BeginEdit
)。因此,这意味着每次执行此调用时,它都会复制并存储行中每一列的每个值。所以你做了 10 次,这意味着你的 1,000 列 DataTable,超过 50,000 行,这意味着它正在分配 500,000,000 个对象。最重要的是,每次更改后都会触发所有其他版本控制、检查和事件,因此,总的来说,当行附加到 DataTable 时比不附加时要慢得多。
看看下面的程序。这是不言自明的,但无论如何我都会解释:)
我有两种方法,一种快,一种慢。这些方法做完全相同的事情:它们创建一个具有 50,000 行和 1000 列的 table。我在 table 中写入可变数量的列。在下面的代码中,我选择了 10 (NUM_COLS_TO_WRITE_TO
).
换句话说,1000 列中只有 10 列实际包含数据。好的。这两种方法的 only 区别在于快速填充列,然后 then 调用 DataTable.AddRow
,而慢速填充列 then,而慢速填充列之后。就是这样。
然而,性能差异令人震惊(无论如何对我来说)。快速版本几乎完全不受更改我们写入的列数的影响,而慢速版本则呈线性上升。例如,当我写入的列数为 20 时,快速版本需要 2.8 秒,但慢速版本需要 分钟。
这里到底发生了什么?
我想也许添加 dt.BeginLoadData
会有所不同,并且在某种程度上确实如此,它使时间从 61 秒减少到大约 50 秒,但这仍然是一个 巨大的 区别。
当然,显而易见的答案是,"Well, don't do it that way."好的。当然。但是到底是什么原因造成的呢?这是预期的行为吗?我当然没想到。 :)
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.BeginEdit
和 theRow.EndEdit
会使慢速版本或多或少保持不变(在我的机器上约为 4 秒)。如果我实际上 对 table 有一些限制,我想这对我来说可能有意义。
附加到 table 后,需要做更多的工作来记录和跟踪每次更改的状态。
例如,如果你这样做,
theRow.BeginEdit();
for (int j = 0; j < NUM_COLS_TO_WRITE_TO; j++)
{
theRow[j] = "whatever";
}
theRow.CancelEdit();
然后在 BeginEdit()
中,internally it's taking a copy of the contents of the row, so that at any point, you can rollback - and the end result of the above is an empty row again without whatever
. This is still possible, even when in BeginLoadData
mode. Following the path ofBeginEdit
if attached to a DataTable, eventually you get into DataTable.NewRecord() 表明它只是复制每一列的每个值以存储原始状态,以防需要取消 - 这里没有太多魔法。另一方面,如果没有附加到数据 table,BeginEdit
中根本不会发生什么,它会很快退出。
EndEdit()
同样很重(附加时),因为这里检查了所有约束等(最大长度、列是否允许空值等)。它还会触发一系列事件,明确释放在编辑被取消的情况下使用的存储空间,并允许使用 DataTable.GetChanges()
进行召回,这在 BeginLoadData
中仍然是可能的。事实上,查看源代码 BeginLoadData
似乎所做的就是关闭约束检查和索引。
所以这描述了 BeginEdit
和 EditEdit
的作用,并且它们在附加或不附加时在存储的内容方面完全不同。现在考虑单个 theRow[j] = "whatever"
您可以在索引器 setter 上看到 DataRow 它调用 BeginEditInternal
然后在每次调用时调用 EditEdit
(如果不是已经在编辑中,因为您之前明确调用了 BeginEdit
)。因此,这意味着每次执行此调用时,它都会复制并存储行中每一列的每个值。所以你做了 10 次,这意味着你的 1,000 列 DataTable,超过 50,000 行,这意味着它正在分配 500,000,000 个对象。最重要的是,每次更改后都会触发所有其他版本控制、检查和事件,因此,总的来说,当行附加到 DataTable 时比不附加时要慢得多。