首页 > 解决方案 > 抛出异常的性能注意事项(重构此模式的最佳方法)

问题描述

我不确定我是否需要在这里或在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)但在实际代码中,有很多对不同字典的调用。如您所见,我的前任喜欢抛出异常。就性能而言,这并不理想,根据微软关于异常和性能的网站,他们建议Tester-Doer Pattern使用Try-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;
}

以及我调用它的方式而不是try/catch block内部的Convert方法PersonConverter

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

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

解决方案 2

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

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:大转换时,根据性能计数器会抛出 750 万次以上的异常# of Exceps Thrown
PS 2:这只是一些示例代码,字典与此示例不同,它永远存在。

标签: c#design-patternsexception-handlinganti-patterns

解决方案


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

性能方面TryGetValue很快,您不需要进行任何优化。请参阅此处的基准:什么更有效:字典 TryGetValue 或 ContainsKey+Item?

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

一般来说,在今天的实现中,输入一个 try 块一点也不昂贵(这并不总是正确的)。但是,抛出和处理异常通常是一项相对昂贵的操作。因此,异常通常应该用于异常事件,而不是正常的流控制。

我们预计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——而不是 using Try-Catch

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

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


推荐阅读