【问题标题】:PHP - Serialize floating pointsPHP - 序列化浮点数
【发布时间】:2010-11-09 17:18:07
【问题描述】:

我在 6 和 8 之间生成 10 个随机浮点数(所有这些都是有充分理由的),并将它们以序列化形式写入 mysql 数据库。但是在存储时似乎出现了一个怪癖:

在存储之前,我只是输出相同的数据以查看它的样子,这就是我得到的结果

a:10:{i:0;d:6.20000000000000017763568394002504646778106689453125;i:1;d:7.5999999999999996447286321199499070644378662109375;i:2;d:6.4000000000000003552713678800500929355621337890625;..}

如您所见,我得到的数字很长,例如 6.20000000000000017763568394002504646778106689453125 而不是我真正喜欢看到的,只是 6.2。仅当我序列化数据时才会发生这种情况,如果我只是输出数组,我会得到浮点数到小数点后一位。 这是我的代码:

function random_float ($min,$max) {
   return ($min+lcg_value()*(abs($max-$min)));
}

$a1 = random_float(6, 8);
$a1 = round($a1, 1);
$a2 = random_float(6, 8);
$a2 = round($a2, 1);    
$a3 = random_float(6, 8);
$a3 = round($a3, 1);
    ...
$array = array($a1, $a2, $a3, $a4, $a5, $a6, $a7, $a8, $a9, $a10);

echo serialize($array);

【问题讨论】:

  • 看起来回声轮会自己漂浮,但这很奇怪

标签: php serialization random floating-point


【解决方案1】:

像 6.2 这样的数字无法在计算机中使用浮点数学精确表示,因为它没有有限的 base-2 表示。当echo-ing 该数字是供人类阅读时所看到的,因此该值将四舍五入为浮点数可以提供的准确度(32 位约 6 位小数,64 位 FP 值约 17 位) )。

然而,在序列化这些值时,您确实需要准确的值(即其中的所有位),而不仅仅是最近的“好”值。可能有多个浮点/双精度表示,其计算结果约为 6.2,并且在序列化时,您通常真的希望将他的确切值存储到您拥有的最后一位,以便正确恢复它们。这就是为什么你在那里的价值观变得荒谬的“准确性”。这一切只是为了保留您开始时的确切位表示。

但是,您究竟为什么要如此严格地控制序列化输出呢?我的意思是,它就在那里,因此您可以往返您的数据结构并稍后再读回它。您当然不想在人类输出的某个地方使用该序列化表示。因此,如果它只是关于“漂亮”的值,则不应使用具有完全不同目的的序列化。

【讨论】:

  • 啊,感谢您的解释,这可能会有很长的路要走。我想让我的序列化数组保持简单的原因是因为我担心更长的序列字符串会总体上增加查询服务器上的负载。我很想知道我错了。感谢其他所有人的及时回复……你们摇滚!只是为了简单起见(即没有额外的功能),我可能会坚持使用这个解决方案,尽管你的 Gumbo 也非常优雅。
  • 嗯,数据库已针对检索数据的用例进行了优化。除非您做一些真正负载密集的事情,否则无论您是读取 200 字节还是 2000 字节都不会产生太大影响。但是当它被证明是一个瓶颈时,你仍然可以优化。
  • 我在存储序列化数据时遇到了这个问题。我正在存储大量记录,因此使用字面上的 58 位数字来存储应该是 4 个字符的内容似乎是不可接受的。我使用了下面shadowhand提供的number_format解决方案。
  • 我通过将精度和 serialize_precision 设置为相同的值 (10) 解决了这个问题:ini_set('precision', 10); ini_set('serialize_precision', 10);你也可以在你的 php.ini 中设置这个
  • 没有答案的解释就像消防员出现在火灾中然后加入旁观者。
【解决方案2】:

使用number_format后将它们存储为字符串:

$number = number_format($float, 2);

【讨论】:

  • number_format 不是将数字转换为字符串的安全方法。 en_US 语言环境将添加分组逗号。 “1234.56”变为“1,234.56”。将一个空字符串附加到要转换的数字。 $number.='';
【解决方案3】:

只是降低精度:

ini_set('serialize_precision',2);

【讨论】:

  • 这应该是一个正确的答案,因为它是正确的解决方案,而不是解决方法。
【解决方案4】:

将它们存储为整数(将小数点前的第一个小数位乘以 10)并在需要时将它们转换回来:

function random_float($min,$max) {
    return ($min+lcg_value()*(abs($max-$min)));
}

$array = array();
for ($i=0; $i<10; $i++) {
    $array[] = (int) round(random_float(6, 8) * 10);
}
$serialized = serialize($array);
var_dump($serialize);

$array = unserialize($serialized);
foreach ($array as $key => $val) {
    $array[$key] = $val / 10;
}
var_dump($array);

【讨论】:

  • 这确实是一个很好的解决方案,但我想我现在会坚持序列化,原因我在上面的评论中写过。感谢您的帮助
【解决方案5】:

Casting有效,而且它更快, 示例:

$a = 0.631;
$b = serialize($a);
$c = serialize((string)$a);
var_dump($b);

字符串(57)“d:0.6310000000000000053290705182007513940334320068359375;”

var_dump($c);

字符串(12) "s:5:"0.631";"

var_dump(unserialize($b));

浮点数(0.631)

var_dump(unserialize($c));

字符串(5)“0.631”

重要的是把它重新反序列化:

var_dump((float)unserialize($c));

浮点数(0.631)

【讨论】:

    【解决方案6】:

    这是我对 Gumbo 答案的看法。我把 IteratorAggregate 放在那里,这样它就可以使用 foreach 了,但你也可以添加 Countable 和 ArrayAccess。

    <?php
    
    class FloatStorage implements IteratorAggregate
    {
      protected $factor;
      protected $store = array();
    
      public function __construct( array $data, $factor=10 )
      {
        $this->factor = $factor;
        $this->store = $data;
      }
    
      public function __sleep()
      {
        array_walk( $this->store, array( $this, 'toSerialized' ) );
        return array( 'factor', 'store' );
      }
    
      public function __wakeup()
      {
        array_walk( $this->store, array( $this, 'fromSerialized' ) );
      }
    
      protected function toSerialized( &$num )
      {
        $num *= $this->factor;
      }
    
      protected function fromSerialized( &$num )
      {
        $num /= $this->factor;
      }
    
      public function getIterator()
      {
        return new ArrayIterator( $this->store );
      }
    }
    
    function random_float ($min,$max) {
       return ($min+lcg_value()*(abs($max-$min)));
    }
    
    $original = array();
    for ( $i = 0; $i < 10; $i++ )
    {
      $original[] = round( random_float( 6, 8 ), 1 );
    }
    
    $stored = new FloatStorage( $original );
    
    $serialized = serialize( $stored );
    $unserialized = unserialize( $serialized );
    
    echo '<pre>';
    print_r( $original );
    print_r( $serialized );
    print_r( $unserialized );
    echo '</pre>';
    

    【讨论】:

      【解决方案7】:

      对我来说,我找到了 3 种方法:

      1. float var 乘以大数(例如 1,000,000)后将 float 转换为整数;这不是一种非常方便的方法,因为您不应该忘记在使用时除以相同的 1,000,000
      2. 使用preg_replace('/d:([0-9]+(\.[0-9]+)?([Ee][+-]?[0-9]+)?);/e', "'d:'.((float)$1).';'", $value); 其中$value 是您的浮动;找到here
      3. 另外,将浮点数舍入round() 并将其作为字符串存储在数组中。

      无论如何我都会使用变体 #2

      【讨论】:

        【解决方案8】:

        PHP.INI 文件包含一个serialize_precision 指令,它允许您控制为您的浮点数序列化多少有效数字。在您的情况下,仅存储 6 到 8 之间的一位小数意味着两位有效数字。

        您可以在 php.ini 文件中或直接在您的脚本中设置此设置:

        ini_set('serialize_precision', 2);
        

        如果您不关心有效数字的确切数量,但不关心浮点数的存储方式导致的数字意大利面条,您也可以选择 -1 的值,这会调用“特殊的舍入算法”,这很可能完全符合要求:

        ini_set('serialize_precision', -1);
        

        您甚至可以在序列化后将其重置为原始值:

            $prec = ini_get('serialize_precision');
            ini_set('serialize_precision', -1);
        
            ... // your serialization here
        
            ini_set('serialize_precision', $prec);
        

        【讨论】:

          【解决方案9】:

          将 php.ini 中的 serialize_precision 值设置为 -1 将解决浮点问题,或者您可以根据此处的规范将其设置为您喜欢的值: http://php.net/manual/en/ini.core.php#ini.serialize-precision

          PHP 版本

          正如其他回复中所指出的,您可以在应用程序本身中覆盖此设置,甚至可以覆盖您的 VirtualHost 容器或 .htaccess 指定的自定义 php.ini。

          希望对你有帮助:)

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2010-12-19
            • 1970-01-01
            • 2013-07-17
            • 1970-01-01
            • 2023-02-20
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多