标量如何存储在perl中的“引擎盖下”?

时间:2016-01-12 19:18:05

标签: perl

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

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

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

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

编辑:

TJD与perlguts相关联,这回答了我的一半问题。标量实际上存储为字符串,int(有符号,无符号,双精度)或指针。我并不太惊讶,我大多数人都期望这种行为发生在引擎盖下,尽管看到确切的类型很有意思。我打开这个问题,因为perlguts实际上是低级别的。然后告诉我存在5种数据类型,它没有指定perl如何在它们之间交替工作,即perl如何决定在保存标量时使用哪种SV类型以及它如何知道何时/如何投射。

3 个答案:

答案 0 :(得分:12)

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

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

Devel::PeekDump是查看标量内部的一个很好的工具。 (SVt_省略了常量前缀SVf_Dump。)

$ 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标志的变量。

答案 1 :(得分:10)

Perl用于数据存储的格式记录在perlguts perldoc。

简而言之,Perl标量存储为SV结构,其中包含多种不同类型之一,例如intdouble,{{1或者指向另一个标量的指针。 (这些类型存储为C char *,因此一次只能存在其中一个; SV包含指示使用哪种类型的标志。)

(关于散列键,有一个重要的问题需要注意:散列键总是字符串,并且总是存储为字符串。它们存储在与其他标量不同的类型中。 )

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

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

答案 2 :(得分:5)

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

my $score = 0;

标量$score将使用整数值进行初始化。但是当这行代码运行时:

say "Your score is $score";

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

这就是为什么Perl运营商倾向于两种口味的原因。例如,使用<==>比较数字值,同时使用lteq,{gt执行相同的字符串比较{1}}。 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会默默地将数值强制转换为字符串)。