将数字数据以二进制形式写入文件还是写出?
Writing numerical data to file as binary vs. written out?
我正在将浮点数写入文件,但是有两种不同的方式来写入这些数字,我想知道该使用哪一种。
两个选项是:
- 将原始代表位写入文件
- 将数字的 ascii 表示写入文件
选项 1 对我来说似乎更实用,因为我将每个浮点数截断为 4 个字节。并且在阅读时可以完全跳过解析每个数字。但在实践中,我只见过使用选项 2。
有问题的数据是 3D 模型信息,小文件大小和快速阅读可能非常有利,但同样,据我所知,没有现有的 3D 模型格式可以做到这一点,我想这一定是有充分理由的后面。
我的问题是,选择写出数字形式而不是位表示的原因是什么?是否存在使用二进制形式更可取的情况?
如果满足以下条件,您可能更喜欢二进制格式:
- 您需要更紧凑的编码(更少的字节 - 因为文本编码可能需要更多 space)。
- 精度 - 因为如果您编码为文本,您可能会失去精度 - 但也许有一些方法可以在不损失精度的情况下编码为文本*。
- 性能可能也是二进制编码的另一个优势。
由于您提到的数据是 3D 模型模拟,编码的紧凑性(可能还有性能)和精度可能与您相关。另一方面,文本编码是人类可读的。
就是说,使用二进制编码通常会遇到诸如字节顺序之类的问题,并且浮点表示在不同的机器上可能会有所不同,但是 here 是一种在便携式计算机中以二进制格式对浮点数(或双精度数)进行编码的方法方式:
uint64_t pack754(long double f, unsigned bits, unsigned expbits)
{
long double fnorm;
int shift;
long long sign, exp, significand;
unsigned significandbits = bits - expbits - 1; // -1 for sign bit
if (f == 0.0) return 0; // get this special case out of the way
// check sign and begin normalization
if (f < 0) { sign = 1; fnorm = -f; }
else { sign = 0; fnorm = f; }
// get the normalized form of f and track the exponent
shift = 0;
while(fnorm >= 2.0) { fnorm /= 2.0; shift++; }
while(fnorm < 1.0) { fnorm *= 2.0; shift--; }
fnorm = fnorm - 1.0;
// calculate the binary form (non-float) of the significand data
significand = fnorm * ((1LL<<significandbits) + 0.5f);
// get the biased exponent
exp = shift + ((1<<(expbits-1)) - 1); // shift + bias
// return the final answer
return (sign<<(bits-1)) | (exp<<(bits-expbits-1)) | significand;
}
*:在 C 中,自从 C99 以来就有 seems 一种方法可以做到这一点,但我仍然认为它需要更多 space.
首先,float
在您通常遇到的任何体系结构上都是 4 个字节,因此当您将这 4 个字节从内存写入文件时,"truncated" 什么都不是。
至于您的主要问题,许多常规文件格式都是为 "interoperability" 和 reading/writing 的易用性而设计的。这就是为什么最常使用几乎普遍可移植的表示形式(尽管存在字符编码问题)的文本。
例如,程序很容易从文本文件中读取字符串“123
”并知道它代表数字 123。
(但请注意,文本本身不是一种格式。您可以选择将所有数据元素表示为 ASCII/Unicode/whatever 字符串,并将所有这些字符串彼此放在一起形成一个文本文件,但是您仍然需要准确指定每个元素的含义以及可以在何处找到哪些数据。例如,非常简单的基于文本的 3D 三角形网格文件格式可能在文件的第一行包含网格中三角形的数量,接下来的 N 行是三个三元组实数,每行指定三角形三个顶点的 X、Y、Z 坐标所需的 9 个数。)
另一方面是二进制格式。这些 通常 中的数据元素与它们在计算机内存中的格式相同。这意味着整数用固定数量的字节(1、2、4 或 8,通常采用 "two's complement" 格式)表示,或者实数用 IEEE 754 格式的 4 或 8 个字节表示。 (请注意,为了切中要点,我省略了很多细节。)
二进制格式的主要优点是:
它们通常尺寸较小。写为 ASCII 字符串的 32 位整数最多可以达到 10 或 11 个字节(例如 -1000000000),但在二进制中它总是占用 4 个字节。更小意味着传输速度更快(通过网络、从磁盘到内存等)并且更易于存储。
每个数据元素的读取速度都更快。不需要复杂的解析。如果数据元素恰好是您的 platform/language 可以使用的确切 format/layout,那么您只需将几个字节从磁盘传输到内存即可。
即使是大型和复杂的数据结构也可以完全在磁盘上布局,就像它们在内存中一样,然后您需要的一切"read" 这种格式的做法是将大量字节(可能包含许多数据元素)从磁盘中获取到内存中,只需一个简单快速的操作,您就完成了。
但是第三个优势要求您将磁盘上的数据布局完全(逐位)与内存中数据结构的布局相匹配。这意味着,几乎总是,该文件格式将仅适用于您的代码,并且仅适用于您的代码,即使您在自己的代码中更改了一些内容,也是如此。这意味着它根本不可移植或不可互操作。但是用起来实在是太快了!
二进制格式也有缺点:
您无法再在像文本编辑器这样的简单通用软件中查看、编辑或理解它们。您可以在任何文本编辑器中打开任何 XML、JSON 或配置文件并很容易地理解它,但不是 JPEG 文件。
与文本格式相比,您通常需要更具体的代码来读取 in/write 二进制格式。更不用说记录文件每一位应该是什么的规范了。文本文件通常更加不言自明和明显。
在某些(许多)语言(脚本和 "higher-level" 语言)中,您通常无权访问构成整数或浮点数的字节,而不是读取它们或写他们。这意味着当您使用 C 或 C++ 等低级语言时,您将失去二进制文件为您提供的大部分速度优势。
基本数据类型的二进制内存格式几乎总是与内存所连接的硬件(或更一般地说,整个平台)相关联。当您选择将内存中的相同位写入文件时,文件格式也变得与硬件相关。一种硬件可能不会以与另一种硬件完全相同的方式存储浮点实数,这意味着写在一个硬件上的二进制文件不能天真地在另一个硬件上读取(必须小心并将数据小心地转换为目标格式。)一个主要区别硬件架构之间的间隔称为 "endianness",它会影响多字节原语(例如 4 字节整数或 8 字节浮点数)预期如何存储在内存中(从最高位字节到最低位字节,反之亦然,分别称为 "big endian" 和 "little endian"。)在大端架构(例如 PowerPC)上写入二进制文件并在小端架构(例如 x86)上逐字读取的数据会将每个基元中的所有字节从高值交换为低值,这意味着所有(好吧,几乎所有)值都是错误的。
既然你提到了3D模型数据,那我就举个典型的游戏引擎使用什么格式的例子。游戏引擎 运行time 很可能需要最快的速度来读取模型,而 3D 模型很大,所以通常它有一个非常具体的模型,而且根本不是可移植的格式文件。但是这种格式很可能不受任何建模软件的支持。因此,您需要编写一个转换器(也称为导出器或导入器),它采用通用的通用格式(例如 OBJ、DAE 等)并将其转换为特定于引擎的专有格式。但正如我提到的,reading/transferring/working-with 基于文本的格式比二进制格式更容易,因此您通常会选择基于文本的通用格式将模型导出到其中,然后 运行 它们上的转换器优化的、二进制的、特定于引擎的 运行 时间格式。
我正在将浮点数写入文件,但是有两种不同的方式来写入这些数字,我想知道该使用哪一种。
两个选项是:
- 将原始代表位写入文件
- 将数字的 ascii 表示写入文件
选项 1 对我来说似乎更实用,因为我将每个浮点数截断为 4 个字节。并且在阅读时可以完全跳过解析每个数字。但在实践中,我只见过使用选项 2。
有问题的数据是 3D 模型信息,小文件大小和快速阅读可能非常有利,但同样,据我所知,没有现有的 3D 模型格式可以做到这一点,我想这一定是有充分理由的后面。
我的问题是,选择写出数字形式而不是位表示的原因是什么?是否存在使用二进制形式更可取的情况?
如果满足以下条件,您可能更喜欢二进制格式:
- 您需要更紧凑的编码(更少的字节 - 因为文本编码可能需要更多 space)。
- 精度 - 因为如果您编码为文本,您可能会失去精度 - 但也许有一些方法可以在不损失精度的情况下编码为文本*。
- 性能可能也是二进制编码的另一个优势。
由于您提到的数据是 3D 模型模拟,编码的紧凑性(可能还有性能)和精度可能与您相关。另一方面,文本编码是人类可读的。
就是说,使用二进制编码通常会遇到诸如字节顺序之类的问题,并且浮点表示在不同的机器上可能会有所不同,但是 here 是一种在便携式计算机中以二进制格式对浮点数(或双精度数)进行编码的方法方式:
uint64_t pack754(long double f, unsigned bits, unsigned expbits)
{
long double fnorm;
int shift;
long long sign, exp, significand;
unsigned significandbits = bits - expbits - 1; // -1 for sign bit
if (f == 0.0) return 0; // get this special case out of the way
// check sign and begin normalization
if (f < 0) { sign = 1; fnorm = -f; }
else { sign = 0; fnorm = f; }
// get the normalized form of f and track the exponent
shift = 0;
while(fnorm >= 2.0) { fnorm /= 2.0; shift++; }
while(fnorm < 1.0) { fnorm *= 2.0; shift--; }
fnorm = fnorm - 1.0;
// calculate the binary form (non-float) of the significand data
significand = fnorm * ((1LL<<significandbits) + 0.5f);
// get the biased exponent
exp = shift + ((1<<(expbits-1)) - 1); // shift + bias
// return the final answer
return (sign<<(bits-1)) | (exp<<(bits-expbits-1)) | significand;
}
*:在 C 中,自从 C99 以来就有 seems 一种方法可以做到这一点,但我仍然认为它需要更多 space.
首先,float
在您通常遇到的任何体系结构上都是 4 个字节,因此当您将这 4 个字节从内存写入文件时,"truncated" 什么都不是。
至于您的主要问题,许多常规文件格式都是为 "interoperability" 和 reading/writing 的易用性而设计的。这就是为什么最常使用几乎普遍可移植的表示形式(尽管存在字符编码问题)的文本。
例如,程序很容易从文本文件中读取字符串“123
”并知道它代表数字 123。
(但请注意,文本本身不是一种格式。您可以选择将所有数据元素表示为 ASCII/Unicode/whatever 字符串,并将所有这些字符串彼此放在一起形成一个文本文件,但是您仍然需要准确指定每个元素的含义以及可以在何处找到哪些数据。例如,非常简单的基于文本的 3D 三角形网格文件格式可能在文件的第一行包含网格中三角形的数量,接下来的 N 行是三个三元组实数,每行指定三角形三个顶点的 X、Y、Z 坐标所需的 9 个数。)
另一方面是二进制格式。这些 通常 中的数据元素与它们在计算机内存中的格式相同。这意味着整数用固定数量的字节(1、2、4 或 8,通常采用 "two's complement" 格式)表示,或者实数用 IEEE 754 格式的 4 或 8 个字节表示。 (请注意,为了切中要点,我省略了很多细节。)
二进制格式的主要优点是:
它们通常尺寸较小。写为 ASCII 字符串的 32 位整数最多可以达到 10 或 11 个字节(例如 -1000000000),但在二进制中它总是占用 4 个字节。更小意味着传输速度更快(通过网络、从磁盘到内存等)并且更易于存储。
每个数据元素的读取速度都更快。不需要复杂的解析。如果数据元素恰好是您的 platform/language 可以使用的确切 format/layout,那么您只需将几个字节从磁盘传输到内存即可。
即使是大型和复杂的数据结构也可以完全在磁盘上布局,就像它们在内存中一样,然后您需要的一切"read" 这种格式的做法是将大量字节(可能包含许多数据元素)从磁盘中获取到内存中,只需一个简单快速的操作,您就完成了。
但是第三个优势要求您将磁盘上的数据布局完全(逐位)与内存中数据结构的布局相匹配。这意味着,几乎总是,该文件格式将仅适用于您的代码,并且仅适用于您的代码,即使您在自己的代码中更改了一些内容,也是如此。这意味着它根本不可移植或不可互操作。但是用起来实在是太快了!
二进制格式也有缺点:
您无法再在像文本编辑器这样的简单通用软件中查看、编辑或理解它们。您可以在任何文本编辑器中打开任何 XML、JSON 或配置文件并很容易地理解它,但不是 JPEG 文件。
与文本格式相比,您通常需要更具体的代码来读取 in/write 二进制格式。更不用说记录文件每一位应该是什么的规范了。文本文件通常更加不言自明和明显。
在某些(许多)语言(脚本和 "higher-level" 语言)中,您通常无权访问构成整数或浮点数的字节,而不是读取它们或写他们。这意味着当您使用 C 或 C++ 等低级语言时,您将失去二进制文件为您提供的大部分速度优势。
基本数据类型的二进制内存格式几乎总是与内存所连接的硬件(或更一般地说,整个平台)相关联。当您选择将内存中的相同位写入文件时,文件格式也变得与硬件相关。一种硬件可能不会以与另一种硬件完全相同的方式存储浮点实数,这意味着写在一个硬件上的二进制文件不能天真地在另一个硬件上读取(必须小心并将数据小心地转换为目标格式。)一个主要区别硬件架构之间的间隔称为 "endianness",它会影响多字节原语(例如 4 字节整数或 8 字节浮点数)预期如何存储在内存中(从最高位字节到最低位字节,反之亦然,分别称为 "big endian" 和 "little endian"。)在大端架构(例如 PowerPC)上写入二进制文件并在小端架构(例如 x86)上逐字读取的数据会将每个基元中的所有字节从高值交换为低值,这意味着所有(好吧,几乎所有)值都是错误的。
既然你提到了3D模型数据,那我就举个典型的游戏引擎使用什么格式的例子。游戏引擎 运行time 很可能需要最快的速度来读取模型,而 3D 模型很大,所以通常它有一个非常具体的模型,而且根本不是可移植的格式文件。但是这种格式很可能不受任何建模软件的支持。因此,您需要编写一个转换器(也称为导出器或导入器),它采用通用的通用格式(例如 OBJ、DAE 等)并将其转换为特定于引擎的专有格式。但正如我提到的,reading/transferring/working-with 基于文本的格式比二进制格式更容易,因此您通常会选择基于文本的通用格式将模型导出到其中,然后 运行 它们上的转换器优化的、二进制的、特定于引擎的 运行 时间格式。