【问题标题】:Performance considerations throwing exceptions (best way to refactor this pattern)抛出异常的性能注意事项(重构此模式的最佳方法)
【发布时间】:2018-07-18 09:15:57
【问题描述】:

我不确定我是否需要在这里或https://softwareengineering.stackexchange.com/ 上问这个问题,但让我们从我目前所拥有的开始。


现状

我正在维护一个数据转换器,它正在逐个记录转换,其中很多(30M+)。 我从旧数据库收到一个 id,插入后我有了新的主键。这些键将存储在某种字典中,因此当我需要查找新 ID 时,我可以找到它。 这是通过以下代码完成的(简化的示例代码,不是真实的)

public class PersonConverter : Converter
{
    public bool Convert(int oldPersonId /*some more parameters*/)
    {
        int newPersonId;

        try
        {
            newPersonId = GetNewPersonIdForPerson(oldPersonId);
        }
        catch (SourceKeyNotFoundException)
        {
            SomeLogging("Log message");
            return true;
        }

        //lots of more thing are happening here!
        return true;
    }
}

public class Converter
{
    private Dictionary<int,int> _convPersonId;

    protected int GetNewPersonIdForPerson(int oldPersonId)
    {
        int key = oldPersonId;
        if (_convPersonId.TryGetValue(key, out int newPersonId))
            return newPersonId;
        throw new SourceKeyNotFoundException(key.ToString());
    }

    protected void SomeLogging(string message)
    {
        //Implement logging etc...
    }
}

PersonConverter 中,我们对GetNewPersonIdForPerson(oldPersonId) 进行了一次调用,但在实际代码中,有很多对不同字典的调用。如您所见,我的前任喜欢抛出异常。性能方面这并不理想,根据微软关于Exceptions & Performance 的网站,他们建议使用Tester-Doer PatternTry-Parse pattern

解决方案

解决方案 1

我想出的解决方案是让GetNewPersonIdForPerson(oldPersonId) 返回int.MinValue 或其他一些确定性值,而不是try/catch block 使用if/else block 检查该值。

抢新方法GetNewPersonIdForPerson2(int oldPersonId)

protected int GetNewPersonIdForPerson2(int oldPersonId)
{
    int key = oldPersonId;
    if (_convPersonId.TryGetValue(key, out int newPersonId))
        return newPersonId;
    return int.MinValue;
}

以及我在ConvertPersonConverter 方法中调用它而不是try/catch block 的方式

if(GetNewPersonIdForPerson2(oldPersonId) != int.MinValue)
{
    newPersonId = GetNewPersonIdForPerson(oldPersonId); 
}
else
{
    SomeLogging("Log message");
    return true;
}

这个解决方案有一些问题,因为我需要调用GetNewPersonIdForPerson2 两次事件,尽管我认为性能方面这比抛出异常要快。

解决方案 2

另一种解决方案是在 GetNewPersonIdForPerson 方法上使用 out 变量,如下所示

protected bool GetNewPersonIdForPerson3(int oldPersonId, out int returnPersonId)
{
    int key = oldPersonId;
    if (_convPersonId.TryGetValue(key, out returnPersonId))
        return true;
    return false;
}

并在 PersonConverter 的 Convert 方法中执行以下操作

if (!GetNewPersonIdForPerson3(oldPersonId, out newPersonId))
{
    SomeLogging("Log message");
    return true;
}

我还没有做任何事情来重构这个,因为我想要一些关于什么是最佳解决方案的输入。我更喜欢Solution 1,因为这更容易重构,但在同一个字典中有 2 次查找。我不能确切地说出我有多少Try/Catch blocks,但有很多。 GetNewPersonIdForPerson 方法不是我唯一拥有的(20+)不知道精确的方法。

问题

谁能告诉我解决这个问题的好模式是什么,或者是我想出的最好的两个解决方案之一。

PS: 对于大转换,根据性能计数器# of Exceps Thrown
PS 2: 这只是一些示例代码和与此示例不同,字典永远存在。

【问题讨论】:

  • 使用解决方案 #1,您可以将 GetNewPersonIdForPerson2(oldPersonId) 放在 if 语句之外的单独变量中,避免执行 2 次。
  • @dlxeon 你说得对,谢谢,没想到那个!但话又说回来,这将是最好的解决方案。
  • 版本#2,可以避免很多不必要的工作。
  • @AlessandroD'Andria 你能解释一下为什么我选择解决方案2可以避免很多不必要的工作吗?
  • @JordyvanEijk 为什么,在第一种情况下,我们需要抛出异常?异常应该代表一种异常情况,我们没有预料到会发生这种情况,这不是我们的情况,抛出异常是一项代价高昂的操作。

标签: c# design-patterns exception-handling anti-patterns


【解决方案1】:

如果您要使用大部分或全部Persons,最好在启动应用程序时(或任何其他运行良好的时间)在初始加载中加载所有用户,然后执行每次需要时快速查找。

性能方面TryGetValue 很快,您不需要进行任何优化。在此处查看基准:What is more efficient: Dictionary TryGetValue or ContainsKey+Item?

在正常情况下,Try-Catch 不应该像这个线程中讨论的那样昂贵:

一般来说,在今天的实现中,输入 try 块并不是 根本不贵(这并不总是正确的)。然而,投掷和 处理异常通常是一项相对昂贵的操作。所以, exceptions 通常应该用于异常事件,而不是正常的 流量控制。

我们预计GetNewPersonIDForPerson() 失败的频率如何?

有了这些信息,我会选择类似的东西:

public class PersonConverter : Converter
{
    private Dictionary<int, int> _IDs;

    public PersonConverter()
    {
        this._IDs = new Dictionary<int, int>();
    }

    public int Convert(int oldPersonID)
    {
        int newPersonID = int.MinValue;
        if (this._IDs.TryGetValue(oldPersonID, out newPersonID))
        {
            /* This oldPerson has been looked up before.
             * The TryGetValue is fast so just let's do that and return the newPersonID */
            return newPersonID;
        }
        else
        {
            try
            {
                /* This oldPerson has NOT been looked up before 
                * so we need to retrieve it from out source and update
                * the dictionary */
                int newPersonID = GetNewPersonIDForPerson(oldPersonID);
                this._IDs.Add(oldPersonID, newPersonID);
                return newPersonID;
            }
            catch (SourceKeyNotFoundException)
            {
                throw;
            }   
        }
    }
}

但是,如果您仍然担心 Try-Catch 语句,我可能会检查 GetNewPersonIDForPerson 方法。如果它对数据库执行查询,如果 oldPersonID 的记录不存在,它可能会返回一些值(0 或 -1),并根据该数据创建我的逻辑 - Tester-Doer - 使用 @987654333 @。

我也可能会进行基准测试,看看在这种情况下使用Try-Catch 语句是否有什么大的不同。如果执行时间有很大差异,我会使用最快的,如果差异可以忽略不计,我会使用最容易理解和维护的代码。

如果真的不值得,我们会尝试优化很多时间。

【讨论】:

  • 由于程序的构建方式和数据提供给我的方式,我无法预先查找所有内容。我无法影响从旧系统(来源)向我提供信息的一方。例如,25 岁​​以上的旧系统在 C++ 中构建,没有关系数据库等。只是循环记录并逐条记录给我。因此,假设源有 3M 订单,有一种方法称为 3M 次。在每次通话中,我都会从源头获取有关 1 条记录的信息。
  • How often do we expect GetNewPersonIDForPerson() to fail? 数百万次大转化!
  • 鉴于您的信息 I 看不到其他解决方案,然后使用 Try-Catch 语句,如果它在 _IDs 字典中不存在,然后存储之间的关系字典中的 ID,以便将来更快地查找。
  • 我现在要做的是,当我将转换后的对象持久保存到数据库时,我将 oldIdnewId 添加到字典中。为简单起见,我将其排除在示例之外
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2014-05-23
  • 2011-04-29
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多