数据结构:图结构
追踪问题
性能问题影响了负责处理媒体会话的服务器产品的新版本。 工程部门使用的自动测试系统与以前的版本相比,性能下降了50%。 幸运的是,该部门采用的流程使他们能够清楚地确定对代码库的所有更改。 具有讽刺意味的是,大多数更改都是性能错误修复。 对变更的审查将候选人的数量减少到两个,然后在每个变更回滚的情况下重复进行性能测试。 这确定了造成性能下降的原因。 幸运的是,这不是性能修复,而是对类的equals
方法的简单修改。
在详细说明问题之前,值得一提的是,该部门可能已使用概要分析工具来确定性能问题的根源。 实际上,这是在引入性能问题很长时间后才发现性能问题的唯一选择。 日常性能测试方法的好处显而易见。
有问题的equals
方法
清单1突出显示了对equals
方法所做的更改。 如您所见,唯一的区别是对其他String
成员变量的求值。 但这真的是造成如此大的性能下降的原因吗?
清单1. equals方法
public boolean equals(Object a_Object)
{
if (a_Object instanceof DialogId)
{
final DialogId d = (DialogId) a_Object;
if (m_CallID.equals(d.m_CallID)
&& compareDestination(m_Destination, d.m_Destination)
&& compareSource(m_Source, d.m_Source)
&& (m_Version == d.m_Version)
&& (m_Identifier.equals(d.m_Identifier)) // NEW
)
{
return true;
}
}
return false;
}
表1显示了被比较的标识符字符串的结构,以及一个示例。
表1.标识符结构
z9hG4bK | IP地址(十六进制) | 港口(十六进制) | 时间(16位十六进制) | 唯一整数 |
---|---|---|---|---|
z9hG4bK | C1C334A7 | 13CE | 000000FC83FAE31 | 11 |
在这种情况下,有两个因素会影响性能:字符串的长度以及前35个字符始终相同的事实。 字符串越长,执行相等性检查所花费的时间就越多,尤其是当字符串仅在结尾附近不同时。 如果更改结构,使序列号移到最前面或缩短字符串,则将获得较小的性能优势。 但是,在这种情况下,这些更改没有帮助。
改进的equals
方法本身不应对性能显着降低负责。 取而代之的是采用equals
方法的方法。
谁使用equals方法?
DialogController
类管理Dialog
类的实例。 DialogController
类间接使用equals方法。 在正常程序流程中,将Dialog
实例插入临时数据结构中,并在对其进行处理后将其移至另一个完整的数据结构中。 DialogController
是一个单例,通常可以管理6,000多个并发Dialog
实例。 清单2显示了DialogController
类的相关部分。
清单2.创建连接的七个方法
public class DialogController
{
private final Map m_Provisional = new HashMap();
private final List m_Completed = new LinkedList();
public boolean handleProvisional(Message a_Message)
{
DialogId d = new Dialog(a_Message);
synchronized(m_SyncMaps)
{
if (m_Completed.containsKey(d))
{
...
}
else if(m_Provisional.contains(d))
{
return true;
}
else
{
m_Provisional.add(d);
return false;
}
}
return true;
}
public boolean handleCompleted(Message a_Message)
{
Dialog d = new Dialog(a_Message);
synchronized(m_SyncMaps)
{
if (m_Provisional.contains(d))
{
m_Provisional.remove(d);
m_Completed.put(d, new Object());
return true;
}
}
return false;
}
}
显然,对两个数据结构的访问是同步的。 在某些程序中,争用锁可能成为性能瓶颈,但在此之前的版本中存在同步。 最明显的选择是使用LinkedList
来保存临时Dialog
实例,考虑到列表中条目的数量,这可能是一个糟糕的选择。 通常,列表在运行时具有数千个条目。 还要注意,完成的实例存储在HashMap
。
性能不佳和equals方法之间的联系在于LinkedList
实现执行contains操作(以及remove操作)的方式。 两者都涉及遍历列表节点,并且针对遇到的每个列表节点执行Dialog equals
方法。 在最坏的情况下,必须检查整个列表。 平均而言,只检查了一半的列表。 有数千个条目,程序在equals方法中比较新的String
成员所花费的累积时间直接导致性能问题,并说明了无害的更改为何会带来严重的后果。
所有这些活动都发生在同步块内的事实也很重要。 在以前的版本中,使用同步块使程序线程安全不会引起问题。 但是在新版本中,增强的equals方法显然突出了此领域作为性能瓶颈。 如果更改了新版本,使其不再是线程安全的,则可以提高性能,尽管是以数据完整性为代价的!
正确的选择
在这种特定情况下,更好的选择是基于散列的集合,它使对象的存储和检索快得多。 此外,Java通过hashCode方法直接在对象模型中支持哈希,该方法由基于哈希的集合使用。
基于哈希的集合旨在提供有效的插入和查找操作。 为了满足这些要求,做出了一些让步。 不需要对集合中的项目进行特定排序。 另外,有效地从容器中移走物品的能力不是主要目标。
现在您已经选择了一个基于哈希的集合,至关重要的是Dialog
类的hashCode
方法生成正确的哈希码。 如果太多的对象共享相同的哈希码,则收集类将诉诸于基于等式的评估。 良好的哈希函数可避免冲突,易于将键均匀分布在数组中,并且计算简便快捷。
相反,单链接列表是所有链接数据结构中最基本的。 它只是一系列动态分配的对象,每个对象都引用其在列表中的后继对象。 添加到头端和尾端既快速又便宜,插入中间位置则更加快捷。 这也有助于分类。 但是,检索列表项可能很昂贵,因为必须遍历和评估每个节点。
使用HashSet
而不是LinkedList
来存储临时Dialog
使性能提高205%。
最后,您已经看到临时Dialog
是如何存储在LinkedList
。 但是,我之前提到了为完成的对话框选择HashMap
。 目前尚不清楚是否由于性能原因做出了这一选择。 之所以选择它,是因为需要在Dialog
旁边存储其他数据。
结论
选择数据结构时,开发人员很容易变得自满或犯下真正的错误。 临时Dialog
的数量可能会随着时间而改变,并且没有重新考虑LinkedList
的适用性。 无论出于何种原因,我都说明了正确或错误数据结构之间的巨大差异。 弄错它会对您的应用程序性能产生可怕的影响。 现在您可以正确处理。
翻译自: https://www.ibm.com/developerworks/web/library/wa-datastruc2/index.html
数据结构:图结构