String.format 慢,需要更快的替代方法

5
我希望能得到一些关于如何加速以下函数的建议。具体来说,我希望找到一种更快的方法将数字(主要是双精度浮点数,我记得有一个整数)转换为字符串以存储为Listview子项。目前,这个函数需要9秒才能处理16个订单!这太疯狂了,尤其是考虑到除了调用处理日期时间的函数之外,它只是字符串转换。
我认为实际显示列表视图项目很慢,所以我进行了一些研究,发现将所有子项添加到数组中并使用Addrange比逐个添加项目要快得多。我实施了这个变化,但没有得到更好的速度。
然后,我在每行代码周围添加了一些秒表,以缩小导致减速的原因;毫不奇怪,调用datetime函数是最大的减速因素,但我惊讶地发现,string.format调用也非常慢,并且由于它们的数量,占据了大部分时间。
    private void ProcessOrders(List<MyOrder> myOrders)
    {
        lvItems.Items.Clear();
        marketInfo = new MarketInfo();
        ListViewItem[] myItems = new ListViewItem[myOrders.Count];
        string[] mySubItems = new string[8];
        int counter = 0;
        MarketInfo.GetTime();
        CurrentTime = MarketInfo.CurrentTime;
        DateTime OrderIssueDate = new DateTime();

        foreach (MyOrder myOrder in myOrders)
        {
            string orderIsBuySell = "Buy";
            if (!myOrder.IsBuyOrder)
                orderIsBuySell = "Sell";
            var listItem = new ListViewItem(orderIsBuySell);

            mySubItems[0] = (myOrder.Name);
            mySubItems[1] = (string.Format("{0:g}", myOrder.QuantityRemaining) + "/" + string.Format("{0:g}", myOrder.InitialQuantity));
            mySubItems[2] = (string.Format("{0:f}", myOrder.Price));
            mySubItems[3] = (myOrder.Local);
            if (myOrder.IsBuyOrder)
            {
                if (myOrder.Range == -1)
                    mySubItems[4] = ("Local");
                else
                    mySubItems[4] = (string.Format("{0:g}", myOrder.Range));
            }
            else
                mySubItems[4] = ("N/A");
            mySubItems[5] = (string.Format("{0:g}", myOrder.MinQuantityToBuy));
            string IssueDateString = (myOrder.DateWhenIssued + " " + myOrder.TimeWhenIssued);
            if (DateTime.TryParse(IssueDateString, out OrderIssueDate))
                mySubItems[6] = (string.Format(MarketInfo.ParseTimeData(CurrentTime, OrderIssueDate, myOrder.Duration)));
            else
                mySubItems[6] = "Error getting date";
            mySubItems[7] = (string.Format("{0:g}", myOrder.ID));
            listItem.SubItems.AddRange(mySubItems);
            myItems[counter] = listItem;
            counter++;

        }
        lvItems.BeginUpdate();
        lvItems.Items.AddRange(myItems.ToArray());
        lvItems.EndUpdate();
    }

这是一个样本运行的时间数据:
0: 166686
1: 264779
2: 273716
3: 136698
4: 587902
5: 368816
6: 955478
7: 128981

这些数字等于数组的索引。除此之外,其他所有行的滴答声都非常低,与这些相比可以忽略不计。
虽然我想使用string.format的数字格式化以获得漂亮的输出,但我更希望能够加载更多的订单列表,如果有一种替代方案比string.format更快但没有花哨的功能,那么我会全力支持它。
编辑:感谢所有建议myOrder类可能使用getter方法而不是存储变量的人。我检查了一下,确实是导致我的减速的原因。虽然我无法访问该类进行更改,但我能够依托于该方法调用来填充myOrders并在同一通话中将每个变量复制到列表中,然后在填充我的Listview时使用该列表。现在几乎瞬间就完成了填充。再次感谢。

我已将此重新标记为 .net 和 c#,因为乍一看它是这样的。希望没问题。 - Jordan
你期望的基准时间是多少?我非常怀疑使用 String.Format() 会花费那么多时间,它应该是可以忽略不计的。我建议你仔细检查一下你的 MarketInfo 方法调用。听起来你将要进行一些 I/O 操作,我怀疑这就是所有的减速来源。 - Jeff Mercado
1
您可能希望在代码示例中包含MyOrder类,或者至少清楚地说明MyOrder的属性是仅为十进制/整数值还是getter方法,在返回值之前可能会执行计算(因为这可能会影响时间解释)。 - Robert Groves
我实际上怀疑 ListView 操作是速度慢的原因。如果你注释掉与 listItem 相关的行,你的函数会加速吗? - Gabe
5个回答

3
我很难相信简单的string.Format调用会导致您的缓慢问题-通常这是非常快速的调用,尤其是像大多数您的那样漂亮简单的调用。
但有一件事可能会让您少花几微秒...
替换
string.Format("{0:g}", myOrder.MinQuantityToBuy)

使用

myOrder.MinQuantityToBuy.ToString("g")

这种方法适用于单个值的简单格式,但对于更复杂的调用不太实用。

是的,这也是我首先想到的事情。使用这个快捷方式将避免每次解析格式字符串,从而在我的机器上节省200ns。这可能不是微秒级别的,但它可能足够重要。 - Gabe
我没有进行基准测试,但这似乎运行得更快。它消除了我在一个非常快速的应用程序中遇到的一些瓶颈。谢谢! - AdamMc331

0

我将所有的string.format调用放进了一个循环中,能在不到一秒的时间内运行1百万次,所以你的问题不是出在string.format上...而是你代码中的其它地方。

也许这些属性中的getter方法包含了逻辑?如果你将listview的所有代码都注释掉,你会得到什么样的时间?


0

绝对不是 string.Format 使您的程序变慢,怀疑来自 myOrder 的属性访问。

在一个格式调用中,尝试声明一些本地变量并将其设置为要格式化的属性,然后将这些本地变量传递给您的 string.Format 并重新计时。您可能会发现,您的 string.Format 现在运行得非常快速了。

现在,属性访问通常不需要太多时间运行。但是,我见过一些类中每个属性访问都被记录下来(用于审计跟踪)。请检查是否是这种情况,以及某些操作是阻止属性访问立即返回的原因。

如果有某些操作阻止属性访问,请尝试排队这些操作(例如排队记录调用),并让后台线程执行它们。立即返回属性访问。

此外,永远不要将耗时的代码(例如复杂的计算)放入属性访问器/获取器中,也不要放入具有副作用的代码。使用该类的人将无法意识到其将变慢(因为大多数属性访问都很快)或具有副作用(因为大多数属性访问没有副作用)。如果访问很慢,请将访问重命名为 GetXXX() 函数。如果有副作用,请将方法命名为能够传达此事实的名称。


0

哇,感觉有点蠢。我花了几个小时碰壁,试图弄清楚为什么一个简单的字符串操作要花那么长时间。MarketOrders是(我以为是)一个由显式调用方法生成的myOrders数组,该方法受到限制,每秒钟可运行的次数非常有限。我无法访问那段代码进行检查,但一直假定myOrders是简单的结构体,当MarketOrders填充时分配其成员变量,因此string.format调用将简单地作用于现有数据上。阅读了所有指向myOrder数据访问的回复后,我开始思考并意识到MarketOrders很可能只是一个索引,而不是一个数组,并且myOrder信息是按需读取的。因此,每次调用其中一个变量的操作,我都在调用缓慢的查找方法,等待它再次变得可运行,然后返回到我的方法,调用下一个查找等等。难怪这需要永远。

感谢所有的回复。我简直不敢相信我没想到这一点。


0

很高兴你解决了你的问题。不过,我对你的方法进行了一些小的重构,并得出了这个:

    private void ProcessOrders(List<MyOrder> myOrders)
    {
        lvItems.Items.Clear();
        marketInfo = new MarketInfo();
        ListViewItem[] myItems = new ListViewItem[myOrders.Count];
        string[] mySubItems = new string[8];
        int counter = 0;
        MarketInfo.GetTime();
        CurrentTime = MarketInfo.CurrentTime;
        // ReSharper disable TooWideLocalVariableScope
        DateTime orderIssueDate;
        // ReSharper restore TooWideLocalVariableScope

        foreach (MyOrder myOrder in myOrders)
        {
            string orderIsBuySell = myOrder.IsBuyOrder ? "Buy" : "Sell";
            var listItem = new ListViewItem(orderIsBuySell);

            mySubItems[0] = myOrder.Name;
            mySubItems[1] = string.Format("{0:g}/{1:g}", myOrder.QuantityRemaining, myOrder.InitialQuantity);
            mySubItems[2] = myOrder.Price.ToString("f");
            mySubItems[3] = myOrder.Local;

            if (myOrder.IsBuyOrder)
                mySubItems[4] = myOrder.Range == -1 ? "Local" : myOrder.Range.ToString("g");
            else
                mySubItems[4] = "N/A";

            mySubItems[5] = myOrder.MinQuantityToBuy.ToString("g");

            // This code smells:
            string issueDateString = string.Format("{0} {1}", myOrder.DateWhenIssued, myOrder.TimeWhenIssued);
            if (DateTime.TryParse(issueDateString, out orderIssueDate))
                mySubItems[6] = MarketInfo.ParseTimeData(CurrentTime, orderIssueDate, myOrder.Duration);
            else
                mySubItems[6] = "Error getting date";

            mySubItems[7] = myOrder.ID.ToString("g");

            listItem.SubItems.AddRange(mySubItems);
            myItems[counter] = listItem;
            counter++;
        }
        lvItems.BeginUpdate();
        lvItems.Items.AddRange(myItems.ToArray());
        lvItems.EndUpdate();
    }

这个方法应该进一步重构:

  1. 使用依赖注入(DI)并以控制反转(IoC)为思想,移除外部依赖;
  2. 为MyOrder创建新属性“DateTimeWhenIssued”,返回DateTime数据类型。这应该代替连接两个字符串(DateWhenIssued和TimeWhenIssued),然后将它们解析为DateTime;
  3. 重命名ListViewItem,因为这是一个内置类;
  4. ListViewItem应该有一个新的构造函数用于布尔值“IsByOrder”:var listItem = new ListViewItem(myOrder.IsBuyOrder)。而不是一个字符串“Buy”或“Sell”;
  5. 应该用一个类来替换“mySubItems”字符串数组,以提高可读性和可扩展性;
  6. 最后,foreach (MyOrder myOrder in myOrders)可以被替换为“for”循环,因为你已经在使用计数器了。此外,“for”循环也更快。

希望您不介意我的建议,并且它们在您的情况下是可行的。

PS. 您是否正在使用泛型数组?ListViewItem.SubItems属性可以是 public List<string> SubItems { get; set; };


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接