【问题标题】:How are scalars stored 'under the hood' in perl?标量如何在 perl 中“隐藏”存储?
【发布时间】:2016-04-17 13:28:25
【问题描述】:

perl 中的基本类型与大多数语言不同,类型为标量、数组、散列(但显然不是子例程,&,我猜这实际上只是带有句法糖的标量引用)。最奇怪的是,最常见的数据类型:int、boolean、char、string,都属于基本数据类型“标量”。似乎 perl 决定根据修改它的运算符将标量视为字符串、布尔值或数字,这意味着标量本身在保存时实际上并未定义为“int”或“String”。

这让我很好奇这些标量是如何“在后台”存储的,特别是关于它对效率的影响(是的,我知道脚本语言会为了灵活性而牺牲效率,但它们仍然需要尽可能优化灵活性问题不受影响)。存储数字 65535(占用两个字节)然后存储占用 6 个字节的字符串“65535”对我来说要容易得多,因此认识到 $val = 65535 正在存储一个 int 将允许我使用 1/3 的内存,在大型数组中,这也可能意味着更少的缓存命中。

当然不仅限于节省内存。如果我知道预期的标量类型,有时我可以提供更重要的优化。例如,如果我有一个使用非常大的整数作为键的散列,如果我将键识别为整数,那么查找一个值会快得多,允许一个简单的模数来创建我的散列键,然后如果我必须运行更复杂的散列具有 3 倍字节的字符串的逻辑。

所以我想知道 perl 如何在后台处理这些标量。它是否将每个值都存储为字符串,在标量始终用作 int 的情况下,牺牲了将字符串常量转换为 int 的额外内存和 cpu 成本?或者它是否有一些逻辑来推断用于确定如何保存和操作它的标量类型?

编辑:

TJD 链接到 perlguts,它回答了我的一半问题。标量实际上存储为字符串、int(有符号、无符号、双精度)或指针。我并不太惊讶,我大多预计这种行为会发生在幕后,尽管看到确切的类型很有趣。不过,我将这个问题悬而未决,因为 perlguts 实际上是低级别的。除了告诉我存在 5 种数据类型之外,它没有指定 perl 如何在它们之间交替工作,即 perl 如何决定在保存标量时使用哪种 SV 类型以及它如何知道何时/如何转换。

【问题讨论】:

  • perlguts 本身包含: 本文档试图描述如何使用 Perl API,并提供 一些 基本工作原理的信息Perl 核心。 它远未完成,可能包含许多错误。请向下面的作者提出任何问题或 cmets。 - 这个问题是相当合法的,应该作为一个世界评论更好地回答。
  • 回复。您的编辑:每个运算符处理其操作数的方式不同。阅读完 perlguts 和 illguts 后,请查看 Perl 源代码中 pp_hot.c 中的一些操作码。 pp_add 很有趣,因为它尽可能地尝试进行整数加法,从而为每个操作数添加一个整数表示。即使您无法遵循代码,这些 cmets 也很有启发性:github.com/Perl/perl5/blob/blead/pp_hot.c#L641。另请查看 pp_print 和 pp_concat。
  • Re: Perl How Perl "alternates between them [types]", the camel book 在第 5 页给出了简洁的解释:“各种运算符期望某些类型的值作为参数,所以我们将讨论这些运算符作为“提供”或“提供”这些参数的标量上下文。有时我们会更具体,说它提供数字上下文、字符串上下文或布尔上下文……”。这意味着运算符没有重载,并且可以从 them 中唯一确定预期的类型。

标签: perl


【解决方案1】:

实际上有多种类型的标量。 SVt_IV 类型的标量可以保存 undef、有符号整数 (IV) 或无符号整数 (UV)。 SVt_PVIV 类型之一也可以保存字符串[1]。标量会根据需要从一种类型静默升级到另一种类型[2]TYPE 字段表示标量的类型。事实上,数组 (SVt_AV) 和散列 (SVt_HV) 实际上只是标量类型。

虽然标量的类型指示标量可以包含什么,但标志用于指示标量确实包含什么。这存储在FLAGS 字段中。 SVf_IOK 表示标量包含有符号整数,而SVf_POK 表示它包含字符串[3]

Devel::PeekDump 是查看标量内部的绝佳工具。 (Dump 省略了常量前缀SVt_SVf_。)

$ perl -e'
   use Devel::Peek qw( Dump );
   my $x = 123;
   Dump($x);
   $x = "456";
   Dump($x);
   $x + 0;
   Dump($x);
'
SV = IV(0x25f0d20) at 0x25f0d30       <-- SvTYPE(sv) == SVt_IV, so it can contain an IV.
  REFCNT = 1
  FLAGS = (IOK,pIOK)                  <-- IOK: Contains an IV.
  IV = 123                            <-- The contained signed integer (IV).

SV = PVIV(0x25f5ce0) at 0x25f0d30     <-- The SV has been upgraded to SVt_PVIV
  REFCNT = 1                              so it can also contain a string now.
  FLAGS = (POK,IsCOW,pPOK)            <-- POK: Contains a string (but no IV since !IOK).
  IV = 123                            <-- Meaningless without IOK.
  PV = 0x25f9310 "456"\0              <-- The contained string.
  CUR = 3                             <-- Number of bytes used by PV (not incl \0).
  LEN = 10                            <-- Number of bytes allocated for PV.
  COW_REFCNT = 1

SV = PVIV(0x25f5ce0) at 0x25f0d30
  REFCNT = 1
  FLAGS = (IOK,POK,IsCOW,pIOK,pPOK)   <-- Now contains both a string (POK) and an IV (IOK).
  IV = 456                            <-- This will be used in numerical contexts.
  PV = 0x25f9310 "456"\0              <-- This will be used in string contexts.
  CUR = 3
  LEN = 10
  COW_REFCNT = 1

illguts 非常彻底地记录了变量的内部格式,但perlguts 可能是一个更好的起点。

如果您开始编写 XS 代码,请记住,检查标量包含的内容通常不是一个好主意。相反,您应该请求应该提供的内容(例如,使用 SvIVSvPVutf8)。 Perl 会自动将值转换为请求的类型(如果合适,会发出警告)。 API 调用记录在perlapi


  1. 事实上,它可以同时保存一个有符号整数或无符号整数的字符串。

  2. 所有标量(包括数组和散列,不包括只能保存 undef 的一种标量)在其基部都有两个内存块。指向标量的指针指向其head,其中包含TYPE 字段和指向body 的指针。升级标量会替换标量的主体。这样,指向标量的指针不会因升级而失效。

  3. undef 变量是一个没有设置任何大写 OK 标志的变量。

【讨论】:

    【解决方案2】:

    Perl 用于数据存储的格式记录在 perlgutsperldoc 中。

    不过,简而言之,Perl 标量存储为 SV 结构,其中包含多种不同类型之一,例如 intdoublechar * 或指向另一个类型的指针标量。 (这些类型存储为 C union,因此一次只会出现其中一个;SV 包含指示使用哪种类型的标志。)

    (关于哈希键,有一个重要的问题需要注意:哈希键始终是字符串,并且始终存储为字符串。它们以与其他标量不同的类型存储。 )

    Perl API 包含许多函数,可用于访问标量值作为所需的 C 类型。例如,SvIV() 可用于返回 SV 的整数值:如果 SV 包含 int,则直接返回该值;如果 SV 包含另一种类型,则会根据需要将其强制转换为整数。这些函数在整个 Perl 解释器中用于类型转换。但是,不会自动推断输出的类型;例如,对字符串进行操作的函数将始终返回 PV(字符串)标量,无论字符串“看起来”是否像数字。

    如果您对给定标量的内部外观感到好奇,可以使用Devel::Peek 模块转储其内容。

    【讨论】:

      【解决方案3】:

      其他人已经解决了您问题的“标量如何存储”部分,因此我将跳过该部分。关于 Perl 如何决定使用哪种表示形式以及何时在它们之间进行转换,答案是取决于将哪些运算符应用于标量。例如,给定以下代码:

      my $score = 0;
      

      标量$score 将被初始化为一个整数值。但是当这行代码运行时:

      say "Your score is $score";
      

      双引号操作符意味着 Perl 需要一个字符串表示的值。因此,从整数到字符串的转换将作为将字符串参数组装到say 函数的过程的一部分进行。有趣的是,在$score 的字符串化之后,标量的底层表示现在将包括一个整数一个字符串表示,允许后续操作直接获取相关值无需再次转换。如果随后将数字运算符应用于字符串(例如:$score++),则将更新数字部分并丢弃(现在无效的)字符串部分。

      这就是 Perl 运算符倾向于有两种风格的原因。例如,比较数字的值是使用&lt;==&gt; 完成的,而与字符串执行相同的比较将使用lteqgt 完成。 Perl 会将标量的值强制转换为与运算符匹配的类型。这就是为什么 + 运算符在 Perl 中进行数字加法,但需要单独的运算符 . 来进行字符串连接:+ 将其参数强制转换为数值,. 将强制转换为字符串。

      有些运算符可以同时处理数字和字符串值,但根据值的类型执行不同的操作。例如:

      $score = 0;
      say ++$score;       # 1
      say ++$score;       # 2
      say ++$score;       # 3
      
      $score = 'aaa';
      say ++$score;       # 'aaa'
      say ++$score;       # 'aab'
      say ++$score;       # 'aac'
      

      关于效率问题(并牢记关于过早优化等的标准免责声明)。考虑一下这段代码,它读取每行包含一个整数的文件,每个整数都经过验证以检查它是否正好是 8 位数字,并且有效的数字存储在一个数组中:

      my @numbers;
      while(<$fh>) {
          if(/^(\d{8})$/) {
              push @numbers, $1;
          }
      }
      

      从文件中读取的任何数据最初都会以字符串的形式提供给我们。用于验证数据的正则表达式还需要$_ 中的字符串值。所以结果是我们的数组@numbers 将包含一个字符串列表。但是,如果值的进一步使用将仅在数字上下文中,我们可以使用此微优化来确保数组仅包含数字值:

      push @numbers, 0 + $1;
      

      在我对 10,000 行文件的测试中,使用字符串填充 @numbers 使用的内存几乎是使用整数值填充的三倍。然而,与大多数基准测试一样,这与 Perl 中的日常编码几乎没有关系。您只需要担心以下情况:a) 存在性能或内存问题,并且 b) 使用大量值。

      值得指出的是,其中一些行为在其他动态语言中很常见(例如:Javascript 会默默地将数值强制转换为字符串)。

      【讨论】:

      • 切向,但0 + $foo 在序列化数据时有更实际的用法(例如使用JSON)。
      猜你喜欢
      • 2011-01-20
      • 1970-01-01
      • 2013-07-12
      • 2017-01-09
      • 2021-12-10
      • 1970-01-01
      • 1970-01-01
      • 2023-03-08
      • 1970-01-01
      相关资源
      最近更新 更多