用PHP中的std :: set等价?

sve*_*ija 7 php c++ set

什么是PHP for C plus和"set"的等效函数("集合是一种存储唯一元素的关联容器,其中元素本身就是键.")?

Ign*_*ams 1

虽然没有,但可以效仿

这是链接失效之前的实现副本..所有内容

PHP: Arraysvs中的一组对象SplObjectStorage

我的一个项目 QueryPath 执行许多需要维护一组唯一对象的任务。在优化 QueryPath 的过程中,我一直在研究以提供便利的包含检查的方式有效存储对象集的各种方法。换句话说,我想要一个数据结构来保存唯一对象的列表,并且可以快速告诉我该列表中是否存在某个对象。循环遍历列表内容的能力也是必要的。

最近我将候选者名单缩小为两种方法:

使用良好的老式数组来模拟哈希集。使用SPLObjectStoragePHP 5.2 及更高版本中的系统。在直接在 中实现任何内容之前QueryPath,我首先开始设计这两种方法,然后对这对方法运行一些微基准测试(在 Crell 的帮助下)。说结果令人惊讶是轻描淡写的。这些基准测试可能会改变我构建未来代码的方式,无论是在Drupal.

设计

在介绍基准之前,我想快速解释一下我确定的两种设计。

模拟哈希集的数组

我一直在考虑的第一种方法是使用 PHP 的标准 array() 来模拟由哈希映射支持的集合(“哈希集”)。集合是一种数据结构,旨在保存唯一元素的列表。就我而言,我感兴趣的是存储一组独特的 DOM 对象。哈希集是使用哈希表实现的集合,其中键是存储值的唯一标识符。虽然人们通常会编写一个类来封装此功能,但我决定将实现作为裸数组进行测试,顶部没有间接层。换句话说,我将要介绍的是哈希集实现的内部结构。

目标:以某种方式存储一组(唯一的)对象,使它们(a)易于迭代,(b)检查成员资格成本低廉。策略:创建一个关联数组,其中键是哈希 ID,值是对象。

有了相当好的散列函数,上面概述的策略应该可以按需要工作。

“相当好的散列函数”——这是第一个陷阱。如何为像这样的对象生成一个好的哈希函数DOMDocument?一种(不好的)方法是序列化对象,然后也许获取其 MD5 哈希值。然而,这不适用于许多对象——特别是任何无法序列化的对象。例如DOMDocument,实际上由资源支持并且无法序列化。

不过,人们需要远远地寻找这样一个函数。事实证明,PHP 5 中有一个对象哈希函数。它称为spl_object_hash(),它可以获取任何对象(甚至不是本机 PHP)并为其生成哈希码。

使用spl_object_hash()我们可以构建一个简单的数据结构,其功能类似于哈希集。这个结构看起来像这样:

array(
  $hashcode => $object
);
Run Code Online (Sandbox Code Playgroud)

例如,我们生成一个像这样的条目:

$object = new stdClass();
$hashcode = spl_object_hash($object);

$arr = array(
  $hashcode => $object
);
Run Code Online (Sandbox Code Playgroud)

那么,在上面的例子中,hashcode字符串就是一个数组键,而对象本身就是数组值。请注意,由于每次重新散列对象时,散列码都是相同的,因此它不仅充当比较点(“如果对象 a 的散列键 == 对象 b 的散列键,则 a == b”),它还充当唯一性约束。每个数组只能存在一个具有指定哈希码的对象,因此不可能将同一对象的两个副本(实际上是两个引用)放置在数组中。

有了这样的数据结构,我们就有了许多现成的函数来操作该结构,因为我们可以使用所有 PHP 数组函数。因此,在某种程度上,这是一个开箱即用的有吸引力的选择。

最重要的任务,至少在我们的上下文中,是确定集合内部是否存在条目。此检查有两种可能的候选者,并且都需要提供哈希码:

使用 array_key_exists() 检查键是否存在。检查是否使用 isset() 设置了密钥。切入主题,isset() 比 array_key_exists() 更快,并且在我们的上下文中提供相同的功能,因此我们将使用它。(它们以不同方式处理空值这一事实对我们来说没有什么区别。不会将空值插入到集合中。)

考虑到这一点,我们将使用如下方式执行遏制检查:

$object = new stdClass();
$hashkey = spl_object_hash($object);
// ...

// Check whether $arr has the $object.
if (isset($arr[$hashkey])) {
  // Do something...
}
Run Code Online (Sandbox Code Playgroud)

同样,使用模拟哈希集的数组允许我们使用所有现有的数组函数,并且还提供简单的可迭代性。我们可以轻松地将其放入 foreach 循环中并迭代内容。不过,在了解其执行情况之前,让我们先看看其他可能的解决方案。

使用 SplObjectStorage

正在考虑的第二种方法使用SplObjectStoragePHP 5.2+ 中的新类(可能在 5.1 中)。该类由 C 实现支持,为类提供类似集合的存储机制。它强制执行唯一性;每个对象只能存储一个。它也是可遍历的,因为它实现了 Iterable 接口。这意味着您可以在 foreach 等循环中使用它。(不利的一面是,PHP 5.2 中的版本不提供任何随机访问或基于索引的访问方法。PHP 5.3 中的版本纠正了这个缺点。)

目标:以某种方式存储一组(唯一的)对象,使它们(a)易于迭代,(b)检查成员资格成本低廉。策略:实例化类的对象SplObjectStorage并存储对其中对象的引用。

创建一个新的 SplObjectStorage 很简单:

$objectStore = new SplObjectStorage();
Run Code Online (Sandbox Code Playgroud)

实例SplObjectStorage不仅保留对象的唯一性信息,而且对象还以可预测的顺序存储。SplObjectStorage是 FIFO——先进先出。

添加对象是通过以下attach()方法完成的:

$objectStore = new SplObjectStorage();
$object = new stdClass();

$objectStore->attach($object);
Run Code Online (Sandbox Code Playgroud)

需要注意的是,attach 只会附加一个对象一次。如果同一个对象被传递attach()两次,第二次尝试将被忽略。因此,没有必要在呼叫contains()之前执行呼叫attach()。这样做是多余的并且成本高昂。

检查 SplObjectStorage 实例中对象是否存在也很简单:

$objectStore = new SplObjectStorage();
$object = new stdClass();

$objectStore->attach($object);

// ...

if ($objectStore->contains($object)) {
  // Do something...
}
Run Code Online (Sandbox Code Playgroud)

虽然SplObjectStorage其支持方法的数量远不及数组可以访问的数量,但它允许迭代以及对存储在其中的对象进行一定程度的有限访问。在许多用例(包括我在这里研究的用例)中,SplObjectStorage提供了必要的功能。

现在我们已经了解了两个候选数据结构,让我们看看它们的性能如何。

比较

任何看过 Larry (Crell) Garfield 的数组和SPL ArrayAccess对象微基准测试的人都可能会带着与 Larry 和我相同的期望进入这组基准测试。我们预计 PHP 的数组会击败 SplObjectStorage。毕竟,数组是 PHP 中的原始类型,并且已经经历了多年的优化。然而,文档SplObjectStorage表明对象的搜索时间SplObjectStorage为 O(1),如果基本速度与数组相似,这肯定会使其具有竞争力。

我的测试环境是:

配备 3.06 Ghz Intel Core 2 Duo 和 2G 800mhz DDR2 RAM 的 iMac(当前一代)。MAMP 1.72 (PHP 5.2.5) 提供了 AMP 堆栈。配备 2.4 Ghz Intel Core 2 Duo 和 4G 667mhz DDR2 RAM 的 MacBook Pro。MAMP 1.72 (PHP 5.2.5) 提供了 AMP 堆栈。在这两种情况下,性能测试的平均值大致相同。本文中的基准来自第二个系统。

我们的基本测试策略是构建一个简单的测试,捕获有关三件事的信息:

加载数据结构所需的时间 查找数据结构所需的时间 数据结构使用的内存量 我们尽力减少其他因素对测试的影响。这是我们的测试脚本:

  <?php
  /**
   * Object hashing tests.
   */
  $sos = new SplObjectStorage();

  $docs = array();
  $iterations = 100000;

  for ($i = 0; $i < $iterations; ++$i) {
    $doc = new DOMDocument();
    //$doc = new stdClass();

    $docs[] = $doc;

  }

  $start = $finis = 0;

  $mem_empty = memory_get_usage();

  // Load the SplObjectStorage
  $start = microtime(TRUE);
  foreach ($docs as $d) {
    $sos->attach($d);
  }
  $finis = microtime(TRUE);

  $time_to_fill = $finis - $start;

  // Check membership on the object storage
  $start = microtime(FALSE);
  foreach ($docs as $d) {
    $sos->contains($d);
  }

  $finis = microtime(FALSE);

  $time_to_check = $finis - $start;

  $mem_spl = memory_get_usage();

  $mem_used = $mem_spl - $mem_empty;

  printf("SplObjectStorage:\nTime to fill: %0.12f.\nTime to check: %0.12f.\nMemory: %d\n\n", $time_to_fill, $time_to_check, $mem_used);

  unset($sos);
  $mem_empty = memory_get_usage();

  // Test arrays:
  $start = microtime(TRUE);
  $arr = array();

  // Load the array
  foreach ($docs as $d) {
    $arr[spl_object_hash($d)] = $d;
  }
  $finis = microtime(TRUE);

  $time_to_fill = $finis - $start;

  // Check membership on the array
  $start = microtime(FALSE);
  foreach ($docs as $d) {
    //$arr[spl_object_hash($d)];
    isset($arr[spl_object_hash($d)]);
  }

  $finis = microtime(FALSE);

  $time_to_check = $finis - $start;
  $mem_arr = memory_get_usage();

  $mem_used = $mem_arr - $mem_empty;


  printf("Arrays:\nTime to fill: %0.12f.\nTime to check: %0.12f.\nMemory: %d\n\n", $time_to_fill, $time_to_check, $mem_used);
  ?>
Run Code Online (Sandbox Code Playgroud)

上面的测试分为四个单独的测试。前两个测试该SplObjectStorage方法处理加载和包含检查的效果。后两个对我们临时设计的数组数据结构执行相同的测试。

上述测试有两点值得注意。

首先,我们选择的测试对象是DOMDocument. 这有几个原因。明显的原因是,此测试是为了优化QueryPathDOM 实现中的元素而进行的。不过,还有另外两个有趣的原因。一是 DOMDocuments 不是轻量级的。另一种是DOMDocuments由 C 实现支持,这使得它们成为存储对象时更困难的情况之一。(例如,它们不能方便地序列化。)

也就是说,在观察结果后,我们用基本stdClass对象重复了测试,发现性能结果几乎相同,并且内存使用量成正比。

第二个值得一提的是我们使用了 100,000 次迭代来进行测试。这大约是我的 PHP 配置在内存耗尽之前允许的上限。但除此之外,这个数字是任意选择的。当我以较低的迭代次数运行测试时,SplObjectStorage绝对会线性缩放。对于较小的数据集,数组性能的可预测性较差(较大的标准差),尽管较小尺寸的数组性能似乎与较大尺寸数组的平均值大致相同(更可预测)。

结果

那么这两种策略在我们的微基准测试中表现如何?以下是运行上述命令时生成的输出的代表性示例:

SplObjectStorage:
Time to fill: 0.085041999817.
Time to check: 0.073099000000.
Memory: 6124624



Arrays:
Time to fill: 0.193022966385.
Time to check: 0.153498000000.
Memory: 8524352
Run Code Online (Sandbox Code Playgroud)

对多次运行进行平均,SplObjectStorage执行填充和检查函数的速度是上面介绍的数组方法的两倍。我们尝试了上述测试的各种排列。例如,这里是使用 a 进行相同测试的结果stdClass object

SplObjectStorage:
Time to fill: 0.082209110260.
Time to check: 0.070617000000.
Memory: 6124624



Arrays:
Time to fill: 0.189271926880.
Time to check: 0.152644000000.
Memory: 8524360
Run Code Online (Sandbox Code Playgroud)

没什么不同。即使向我们存储的对象添加任意数据也不会影响所需的时间SplObjectStorage(尽管它似乎确实稍微增加了数组的时间)。

我们的结论是,SplObjectStorage对于在集合中存储大量对象来说,这确实是一个更好的解决方案。上周,我已经移植QueryPathSplObjectStorage(请参阅 GitHub 上的 Quark 分支——现有Drupal QueryPath模块可以使用这个实验分支而无需更改),并且可能会继续进行基准测试。但初步结果似乎为最佳方法提供了明确的指示。

由于这些发现,我不太愿意仅仅因为数组是基本数据类型而将其默认为“最佳选择”。如果SPL库包含性能优于数组的功能,则应在适当的时候使用它们。从 QueryPath 到我的 Drupal 模块,我预计我的代码将受到这些发现的影响。

感谢 Crell 的帮助,感谢 Frameweld 的 Eddie 首先激发了我对这两种方法的研究。