问题
很长一段时间以来,我的印象是使用嵌套的std::vector
g 5.2
dimensions nested flat
X Y Z (ms) (ms)
100 100 100 -> 16 24
150 150 150 -> 58 98
200 200 200 -> 136 308
250 250 250 -> 264 746
300 300 300 -> 440 1537
叮当声(LLVM 7.0.0)
dimensions nested flat
X Y Z (ms) (ms)
100 100 100 -> 16 18
150 150 150 -> 53 61
200 200 200 -> 135 137
250 250 250 -> 255 271
300 300 300 -> 423 477
正如您所看到的,“展平”包装器从来没有打败嵌套版本。此外,与libc实现相比,g的libstdc实现的性能非常差,例如
300x300x300
扁平化版本几乎比嵌套版本慢4倍。libc似乎也有同样的表现。
我的问题是:
为什么扁平版不是更快?不应该是吗?我在测试代码中遗漏了什么吗?
- 此外,为什么g的libstdc在使用flatten向量时表现如此糟糕?再说一次,它不应该表现得更好吗?
我使用的代码:
#include <chrono>
#include <cstddef>
#include <iostream>
#include <memory>
#include <random>
#include <vector>
// Thin wrapper around flatten vector
template<typename T>
class Array3D
{
std::size_t _X, _Y, _Z;
std::vector<T> _vec;
public:
Array3D(std::size_t X, std::size_t Y, std::size_t Z):
_X(X), _Y(Y), _Z(Z), _vec(_X * _Y * _Z) {}
T& operator()(std::size_t x, std::size_t y, std::size_t z)
{
return _vec[z * (_X * _Y) + y * _X + x];
}
const T& operator()(std::size_t x, std::size_t y, std::size_t z) const
{
return _vec[z * (_X * _Y) + y * _X + x];
}
};
int main(int argc, char** argv)
{
std::random_device rd{};
std::mt19937 rng{rd()};
std::uniform_real_distribution<double> urd(-1, 1);
const std::size_t X = std::stol(argv[1]);
const std::size_t Y = std::stol(argv[2]);
const std::size_t Z = std::stol(argv[3]);
// Standard library nested vector
std::vector<std::vector<std::vector<double>>>
vec3D(X, std::vector<std::vector<double>>(Y, std::vector<double>(Z)));
// 3D wrapper around a 1D flat vector
Array3D<double> vec1D(X, Y, Z);
// TIMING nested vectors
std::cout << "Timing nested vectors...\n";
auto start = std::chrono::steady_clock::now();
volatile double tmp1 = 0;
for (std::size_t x = 0 ; x < X; ++x)
{
for (std::size_t y = 0 ; y < Y; ++y)
{
for (std::size_t z = 0 ; z < Z; ++z)
{
vec3D[x][y][z] = urd(rng);
tmp1 += vec3D[x][y][z];
}
}
}
std::cout << "\tSum: " << tmp1 << std::endl; // we make sure the loops are not optimized out
auto end = std::chrono::steady_clock::now();
std::cout << "Took: ";
auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();
std::cout << ms << " milliseconds\n";
// TIMING flatten vector
std::cout << "Timing flatten vector...\n";
start = std::chrono::steady_clock::now();
volatile double tmp2 = 0;
for (std::size_t x = 0 ; x < X; ++x)
{
for (std::size_t y = 0 ; y < Y; ++y)
{
for (std::size_t z = 0 ; z < Z; ++z)
{
vec1D(x, y, z) = urd(rng);
tmp2 += vec1D(x, y, z);
}
}
}
std::cout << "\tSum: " << tmp2 << std::endl; // we make sure the loops are not optimized out
end = std::chrono::steady_clock::now();
std::cout << "Took: ";
ms = std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();
std::cout << ms << " milliseconds\n";
}
编辑
更改
阵列3D
return _vec[(x * _Y + y) * _Z + z];
根据@1201programalm的建议,确实消除了g的“奇怪”行为,从这个意义上讲,平面版本和嵌套版本现在所用的时间大致相同。然而,这仍然很有趣。我认为嵌套的会因为缓存问题而变得更糟。我能幸运地把所有的内存连续分配吗?
在阅读其他答案时,我对答案的准确性和详细程度并不满意,因此我将尝试自己进行解释:
这里的人的问题不是间接的,而是空间位置的问题:
基本上有两件事使缓存特别有效:
>
空间局部性,这意味着如果一个记忆词已经被访问,很可能这个词之前或之后的记忆词也将很快被访问。对于嵌套和扁平数组,这种情况会发生。
为了评估间接和缓存效应对这个问题的影响,让我们假设我们有X=Y=Z=1024
从这个问题判断,单个缓存线(L1、L2或L3)的长度为64字节,即8个双值。假设一级缓存有32KB(4096倍),二级缓存有256KB(32k倍),三级缓存有8MB(1M倍)。
这意味着-假设缓存中没有其他数据(我知道这是一个大胆的猜测)-在平坦的情况下,y
的每4个值只会导致一级缓存未命中(二级缓存延迟可能约为10-20个周期),只有y
的第32个值才会导致二级缓存未命中(三级缓存延迟的某个值低于100个周期),并且只有在三级缓存未命中的情况下,我们才必须访问主内存。我不想在这里打开整个计算,因为考虑到整个缓存层次结构会让它变得有点困难,但我们可以说,几乎所有对内存的访问都可以在扁平化的情况下缓存。
在这个问题的原始公式中,展平索引的计算方式不同(z*(\ux*\uy)Y*\ux
),最里面的循环(z)中的值的增加总是意味着\ux*\uy*64位
的跳跃,从而导致更非本地的内存布局,这大大增加了缓存故障。
在嵌套的情况下,答案很大程度上取决于Z的值:
向量中单个向量的条目
由于有一个关于装配输出的问题,让我简要概述一下:
如果比较嵌套数组和展平数组的程序集输出,您会注意到许多相似之处:有三个等效的嵌套循环,计数变量x、y和z存储在寄存器中。唯一真正的区别——除了嵌套版本对每个外部索引使用两个计数器以避免在每次地址计算时乘以24之外,展平版本对最里面的循环也一样,乘以8-可以在y循环中找到,在y循环中,不只是增加y并计算展平索引,我们需要执行三个相互依赖的内存加载来确定内部循环的基指针:
mov rax, QWORD PTR [rdi]
mov rax, QWORD PTR [rax+r11]
mov rax, QWORD PTR [rax+r10]
但由于这些只在第z次发生,并且“中间向量”的指针很可能是缓存的,因此时间差可以忽略不计。
这是因为您在3D类中如何排序索引。因为最内部的循环正在改变z,这是索引的最大部分,所以你会有很多缓存失败。重新排列您的索引
_vec[(x * _Y + y) * _Z + z]
你应该看到更好的表现。
在固定索引顺序后,为什么嵌套向量的速度与微基准中的平面差不多:你会期望平面数组更快(参见Tobias关于潜在局部性问题的回答,以及我关于嵌套向量总体上很糟糕但不太糟糕的另一个回答严重的顺序访问)。但是您的特定测试做了很多事情,使得无序执行隐藏了使用嵌套向量的开销,并且/或者只是减慢了速度,以至于额外的开销在测量噪声中丢失了。
我把你的性能错误修复源代码放在戈德博尔特上,这样我们就可以看到由g 5.2编译的内部循环的最大值,带有-O3
。苹果的clang分叉可能类似于clang3.7,但我只看gcc版本。)有很多来自C函数的代码,但是您可以右键单击源代码行,将ASM窗口滚动到该行的代码中。此外,鼠标悬停在源代码行上,以加粗实现该行的ASM,反之亦然。
嵌套版本的GCC内部两个循环如下(手动添加了一些注释):
## outer-most loop not shown
.L213: ## middle loop (over `y`)
test rbp, rbp # Z
je .L127 # inner loop runs zero times if Z==0
mov rax, QWORD PTR [rsp+80] # MEM[(struct vector * *)&vec3D], MEM[(struct vector * *)&vec3D]
xor r15d, r15d # z = 0
mov rax, QWORD PTR [rax+r12] # MEM[(struct vector * *)_195], MEM[(struct vector * *)_195]
mov rdx, QWORD PTR [rax+rbx] # D.103857, MEM[(double * *)_38]
## Top of inner-most loop.
.L128:
lea rdi, [rsp+5328] # tmp511, ## function arg: pointer to the RNG object, which is a local on the stack.
lea r14, [rdx+r15*8] # D.103851, ## r14 = &(vec3D[x][y][z])
call double std::generate_canonical<double, 53ul, std::mersenne_twister_engine<unsigned long, 32ul, 624ul, 397ul, 31ul, 2567483615ul, 11ul, 4294967295ul, 7ul, 2636928640ul, 15ul, 4022730752ul, 18ul, 1812433253ul> >(std::mersenne_twister_engine<unsigned long, 32ul, 624ul, 397ul, 31ul, 2567483615ul, 11ul, 4294967295ul, 7ul, 2636928640ul, 15ul, 4022730752ul, 18ul, 1812433253ul>&) #
addsd xmm0, xmm0 # D.103853, D.103853 ## return val *= 2.0: [0.0, 2.0]
mov rdx, QWORD PTR [rsp+80] # MEM[(struct vector * *)&vec3D], MEM[(struct vector * *)&vec3D] ## redo the pointer-chasing from vec3D.data()
mov rdx, QWORD PTR [rdx+r12] # MEM[(struct vector * *)_150], MEM[(struct vector * *)_150]
subsd xmm0, QWORD PTR .LC6[rip] # D.103859, ## and subtract 1.0: [-1.0, 1.0]
mov rdx, QWORD PTR [rdx+rbx] # D.103857, MEM[(double * *)_27]
movsd QWORD PTR [r14], xmm0 # *_155, D.103859 # store into vec3D[x][y][z]
movsd xmm0, QWORD PTR [rsp+64] # D.103853, tmp1 # reload volatile tmp1
addsd xmm0, QWORD PTR [rdx+r15*8] # D.103853, *_62 # add the value just stored into the array (r14 = rdx+r15*8 because nothing else modifies the pointers in the outer vectors)
add r15, 1 # z,
cmp rbp, r15 # Z, z
movsd QWORD PTR [rsp+64], xmm0 # tmp1, D.103853 # spill tmp1
jne .L128 #,
#End of inner-most loop
.L127: ## middle-loop
add r13, 1 # y,
add rbx, 24 # sizeof(std::vector<> == 24) == the size of 3 pointers.
cmp QWORD PTR [rsp+8], r13 # %sfp, y
jne .L213 #,
## outer loop not shown.
对于扁平环:
## outer not shown.
.L214:
test rbp, rbp # Z
je .L135 #,
mov rax, QWORD PTR [rsp+280] # D.103849, vec1D._Y
mov rdi, QWORD PTR [rsp+288] # D.103849, vec1D._Z
xor r15d, r15d # z
mov rsi, QWORD PTR [rsp+296] # D.103857, MEM[(double * *)&vec1D + 24B]
.L136: ## inner-most loop
imul rax, r12 # D.103849, x
lea rax, [rax+rbx] # D.103849,
imul rax, rdi # D.103849, D.103849
lea rdi, [rsp+5328] # tmp520,
add rax, r15 # D.103849, z
lea r14, [rsi+rax*8] # D.103851, # &vec1D(x,y,z)
call double std::generate_canonical<double, 53ul, std::mersenne_twister_engine<unsigned long, 32ul, 624ul, 397ul, 31ul, 2567483615ul, 11ul, 4294967295ul, 7ul, 2636928640ul, 15ul, 4022730752ul, 18ul, 1812433253ul> >(std::mersenne_twister_engine<unsigned long, 32ul, 624ul, 397ul, 31ul, 2567483615ul, 11ul, 4294967295ul, 7ul, 2636928640ul, 15ul, 4022730752ul, 18ul, 1812433253ul>&) #
mov rax, QWORD PTR [rsp+280] # D.103849, vec1D._Y
addsd xmm0, xmm0 # D.103853, D.103853
mov rdi, QWORD PTR [rsp+288] # D.103849, vec1D._Z
mov rsi, QWORD PTR [rsp+296] # D.103857, MEM[(double * *)&vec1D + 24B]
mov rdx, rax # D.103849, D.103849
imul rdx, r12 # D.103849, x # redo address calculation a 2nd time per iteration
subsd xmm0, QWORD PTR .LC6[rip] # D.103859,
add rdx, rbx # D.103849, y
imul rdx, rdi # D.103849, D.103849
movsd QWORD PTR [r14], xmm0 # MEM[(double &)_181], D.103859 # store into the address calculated earlier
movsd xmm0, QWORD PTR [rsp+72] # D.103853, tmp2
add rdx, r15 # tmp374, z
add r15, 1 # z,
addsd xmm0, QWORD PTR [rsi+rdx*8] # D.103853, MEM[(double &)_170] # tmp2 += vec1D(x,y,z). rsi+rdx*8 == r14, so this is a reload of the store this iteration.
cmp rbp, r15 # Z, z
movsd QWORD PTR [rsp+72], xmm0 # tmp2, D.103853
jne .L136 #,
.L135: ## middle loop: increment y
add rbx, 1 # y,
cmp r13, rbx # Y, y
jne .L214 #,
## outer loop not shown.
你的MacBook Pro(2012年底)有一个Intel IvyBridge CPU,所以我使用Agner Fog的指令表和Microach指南中的数字来表示微体系结构。其他Intel/AMD CPU上的情况应该大致相同。
唯一的2.5GHz移动IvB i5是i5-3210M,因此您的CPU具有3MB的三级缓存。这意味着即使是最小的测试用例(每个double
~=7.63MiB 100^3*8B)也比上一级缓存大,因此没有一个测试用例适合缓存。这可能是一件好事,因为在测试嵌套和平面之前,您可以对它们进行分配和默认初始化。但是,测试的顺序与分配的顺序相同,因此,如果将平面阵列归零后,嵌套阵列仍然是缓存,则在嵌套阵列上的定时循环之后,平面阵列在三级缓存中可能仍然是热的。
如果您使用重复循环多次循环相同的数组,您可以获得足够大的次数来测量较小的数组大小。
你在这里做了一些非常奇怪的事情,使其变得如此缓慢,以至于无序执行可以隐藏更改y
的额外延迟,即使你的内部z
向量不是完全连续的。
>
在定时循环中运行一个慢速PRNG。std::uniform_real_distribution
G5.2没有完全内联
urd(rng)
代码,并且x86-64 System V调用约定没有保留调用的XMM寄存器。因此,必须为每个元素溢出/重新加载tmp1
/tmp2
,即使它们不是易失性的。
它也失去了它在Z向量中的位置,并且在访问下一个
z
元素之前必须重做外部2级间接。这是因为它不知道它调用的函数的内部,并假设它可能有一个指向外部向量的指针
clang(使用libc)完全内联PRNG,因此移动到下一个
z
只是add reg,8
在平面和嵌套版本中增加指针。通过在内部循环外获取迭代器,或者获取对内部向量的引用,而不是重做操作符[]
并希望编译器为您提升它,您可以从gcc获得相同的行为。
Intel/AMD FP add/sub/mul吞吐量/延迟与数据无关,非规范化除外。(x87对于NaN和无穷大也会减慢速度,但SSE不会。64位代码甚至对标量
浮点
/
双精度
也使用SSE)所以你可以用零初始化你的数组,或者用一个PRNG输出定时循环。(或者将它们置零,因为
向量
在读取元素之前,在内部循环中写入每个元素。在源代码中,这看起来像是存储/重新加载。不幸的是,gcc实际上就是这样做的,但与libc的碰撞(将PRNG内联)会改变循环体:
// original
vec3D[x][y][z] = urd(rng);
tmp1 += vec3D[x][y][z];
// what clang's asm really does
double xmm7 = urd(rng);
vec3D[x][y][z] = xmm7;
tmp1 += xmm7;
在clang的asm中:
# do { ...
addsd xmm7, xmm4 # last instruction of the PRNG
movsd qword ptr [r8], xmm7 # store it into the Z vector
addsd xmm7, qword ptr [rsp + 88]
add r8, 8 # pointer-increment to walk along the Z vector
dec r13 # i--
movsd qword ptr [rsp + 88], xmm7
jne .LBB0_74 # }while(i != 0);
允许这样做是因为vec3D不是易失性的或原子的
在gcc版本中,它在PRNG调用之前为存储建立索引,在PRNG调用之后为重载建立索引。因此,我认为gcc不确定函数调用是否修改指针,因为指向外部向量的指针已从函数中转义。(而且PRNG不在线)。
但是,即使是asm中的实际存储/重新加载,对缓存未命中的敏感度也不如简单加载!
贮藏-
对于反向索引(扁平代码的原始版本),主要的瓶颈可能是分散的存储。为什么clang比gcc做得更好?也许clang最终成功地反转了一个循环并按顺序遍历了内存。(因为它完全内联了PRNG,所以没有函数调用需要内存状态与程序顺序相匹配。)
按顺序遍历每个Z向量意味着缓存缺失相对较远(即使每个Z向量与前一个向量不相邻),为存储提供了大量执行时间。或者,即使在L1D缓存实际拥有缓存行之前(处于MESI协议的修改状态),存储转发负载实际上不能退出,推测执行也具有正确的数据,并且不必等待缓存未命中的延迟。无序指令窗口可能足够大,以防止关键路径在负载退出之前停顿。(缓存未命中负载通常非常糟糕,因为没有数据可供操作的依赖指令无法执行。所以他们更容易在管道中制造泡沫。由于DRAM的完全缓存缺失具有超过300个周期的延迟,并且IvB上的乱序窗口为168 uops,因此它不能隐藏以每时钟1 uop(大约1个指令)执行的代码的所有延迟。(对于纯商店,乱序窗口超出了ROB大小,因为它们不需要promiseL1D才能退休。事实上,他们直到退休后才能做出promise,因为那是他们被认为是非投机性的时候。因此,在更早的时候让它们全局可见,可以防止在检测到异常或错误猜测时回滚。)
我的桌面上没有安装
libc
,所以我不能用g作为基准测试那个版本。对于g5.4,我发现嵌套: 225毫秒,扁平: 239毫秒。我怀疑额外的数组索引倍数是一个问题,并与PRNG使用的ALU指令竞争。相比之下,嵌套版本重做L1D缓存中命中的一堆指针追逐可以并行发生。我的桌面是Skylake i7-6700k4.4GHz。SKL的ROB(重新排序缓冲区)大小为224 uops,RS为97 uops,因此乱序窗口非常大。它还具有FP-add延迟4个周期(不同于以前的3个周期)。
volatile double tmp1=0
累加器是易变的,这迫使编译器在内部循环的每次迭代中存储/重新加载累加器。内部循环中循环携带的依赖项链的总延迟为9个周期:3个用于
addsd
,6个用于从movsd
存储转发到movsd
重新加载。(使用addsd xmm7,qword ptr[rsp 88]
将重载折叠到内存操作数中,但区别相同。([rsp 88]
位于堆栈上,如果需要从寄存器溢出,则在堆栈中存储具有自动存储功能的变量。)
如上所述,对gcc的非内联函数调用也将强制x86-64系统V调用约定中的溢出/重新加载(除了Windows之外,其他所有程序都使用)。但是一个聪明的编译器可以执行4个PRNG调用,然后执行4个数组存储。(如果您使用迭代器来确保gcc知道持有其他向量的向量没有变化。)
使用
-ffast数学
会让编译器自动矢量化(如果不是针对PRNG和易失性
)。这将使您能够足够快地运行数组,不同Z向量之间缺乏局部性可能是一个真正的问题。它还将允许编译器使用多个累加器展开,以隐藏FP-add延迟。例如,他们可以(并且clang会)使ASM等同于:
float t0=0, t1=0, t2=0, t3=0;
for () {
t0 += a[i + 0];
t1 += a[i + 1];
t2 += a[i + 2];
t3 += a[i + 3];
}
t0 = (t0 + t1) + (t2 + t3);
它有4个独立的依赖链,因此可以保留4个FP添加。由于IvB有3个周期延迟,对于
addsd
,每时钟一个吞吐量,因此我们只需要保持4个周期延迟以饱和其吞吐量。(Skylake有4c延迟,每时钟2个吞吐量,与mul或FMA相同,因此需要8个累加器来避免延迟瓶颈。事实上,更多的累加器更好。该问题提问者的测试表明,Haswell在接近最大负载吞吐量时使用更多累加器做得更好。)
类似这样的测试可以更好地测试在Array3D上循环的效率。如果您想阻止循环完全优化,只需使用结果即可。测试你的微基准,以确保增加问题的规模可以延长时间;如果没有,那么一些东西就被优化了,或者你没有测试你认为你正在测试的东西。不要使内部循环成为临时的
易变的
!!
编写微基准并不容易。你必须有足够的理解能力来编写一个测试你认为你正在测试的东西的工具这是一个很好的例子,说明出错是多么容易。
我能幸运地把所有的内存连续分配吗?
是的,这可能会发生在许多按顺序完成的小型分配中,而在执行此操作之前,您还没有分配和释放任何内容。如果它们足够大(通常是一个4KB或更大的页面),glibc
malloc
将切换到使用mmap(MAP\u ANONYMOUS)
,然后内核将选择随机虚拟地址(ASLR)。因此,随着Z的增大,您可能会期望局部性变得更差。但另一方面,较大的Z向量意味着在一个连续向量上循环的时间更多,因此更改y
(和x
)时的缓存未命中变得相对不那么重要。
在数据上按顺序循环显然不会暴露这一点,因为额外的指针访问命中缓存,所以指针跟踪的延迟足够低,OOO执行可以用缓慢的循环隐藏它。
预回迁在这里很容易保持。
不同的编译器/库会对这个奇怪的测试产生很大的影响。在我的系统(Arch Linux,i7-6700k Skylake和4.4GHz max turbo)上,G5.4-O3在
300
下运行4次的最好结果是:
Timing nested vectors...
Sum: 579.78
Took: 225 milliseconds
Timing flatten vector...
Sum: 579.78
Took: 239 milliseconds
Performance counter stats for './array3D-gcc54 300 300 300':
532.066374 task-clock (msec) # 1.000 CPUs utilized
2 context-switches # 0.004 K/sec
0 cpu-migrations # 0.000 K/sec
54,523 page-faults # 0.102 M/sec
2,330,334,633 cycles # 4.380 GHz
7,162,855,480 instructions # 3.07 insn per cycle
632,509,527 branches # 1188.779 M/sec
756,486 branch-misses # 0.12% of all branches
0.532233632 seconds time elapsed
与G7.1-O3相比(它显然决定在G5.4没有的东西上分支)
Timing nested vectors...
Sum: 932.159
Took: 363 milliseconds
Timing flatten vector...
Sum: 932.159
Took: 378 milliseconds
Performance counter stats for './array3D-gcc71 300 300 300':
810.911200 task-clock (msec) # 1.000 CPUs utilized
0 context-switches # 0.000 K/sec
0 cpu-migrations # 0.000 K/sec
54,523 page-faults # 0.067 M/sec
3,546,467,563 cycles # 4.373 GHz
7,107,511,057 instructions # 2.00 insn per cycle
794,124,850 branches # 979.299 M/sec
55,074,134 branch-misses # 6.94% of all branches
0.811067686 seconds time elapsed
vs. clang4.0-O3(使用gcc的libstdc,而不是libc)
perf stat ./array3D-clang40-libstdc++ 300 300 300
Timing nested vectors...
Sum: -349.786
Took: 1657 milliseconds
Timing flatten vector...
Sum: -349.786
Took: 1631 milliseconds
Performance counter stats for './array3D-clang40-libstdc++ 300 300 300':
3358.297093 task-clock (msec) # 1.000 CPUs utilized
9 context-switches # 0.003 K/sec
0 cpu-migrations # 0.000 K/sec
54,521 page-faults # 0.016 M/sec
14,679,919,916 cycles # 4.371 GHz
12,917,363,173 instructions # 0.88 insn per cycle
1,658,618,144 branches # 493.887 M/sec
916,195 branch-misses # 0.06% of all branches
3.358518335 seconds time elapsed
我没有深入研究clang做错了什么,也没有尝试使用
-ffast math
和/或-march=native
。(不过,除非您删除volatile
,否则这些功能不会有多大用处。)
perf stat-d
对于clang,不会显示比gcc更多的缓存未命中(一级或最后一级)。但它确实表明,clang的L1D负载是L1D负载的两倍多。
我试过使用非正方形数组。这几乎与保持元素总数不变,但将最终尺寸更改为5或6完全相同。
即使对C进行微小的更改也会有所帮助,并使“扁平化”比gcc嵌套更快(从240毫秒下降到200毫秒,但对嵌套来说几乎没有任何区别。):
// vec1D(x, y, z) = urd(rng);
double res = urd(rng);
vec1D(x, y, z) = res; // indexing calculation only done once, after the function call
tmp2 += vec1D(x, y, z);
// using iterators would still avoid redoing it at all.
矢量或者说向量,可以通过2~4个分量表示一个向量,比如通过vec3(1,0,0)表示三维空间中一个沿着x轴正方向的三维方向向量,如果你有高中数学的基础,应该对向量有一定的了解,对于三维坐标的相关几何运算也有一定的概念。 关键字 数据类型 vec2 二维向量,具有xy两个分量,分量是浮点数 vec3 三维向量 ,具有xyz三个分量,分量是浮点数 vec4 四维向量 ,具有xyzw四个分量,分量是浮点
Vector是由连续整数索引的值的集合。 使用Clojure中的矢量方法创建矢量。 例子 (Example) 以下是在Clojure中创建矢量的示例。 (ns clojure.examples.example (:require [clojure.set :as set]) (:gen-class)) (defn example [] (println (vector 1 2 3
矢量瓦片是将矢量数据通过不同的描述文件来组织和定义,在客户端实时解析数据和完成绘制。SuperMap iServer 提供了矢量瓦片图层源,即 ol.source.VectorTileSuperMapRest.optionsFromMapJSON(url,mapJSONObj) 其中: url:地图服务地址 mapJSONObj:地图JSON对象(由 getMapInfor() 方法返回的 JSO
结构体定义、常量和构造函数 定义: typedef struct cpVect{ cpFloat x, y; } cpVect 零向量常量: static const cpVect cpvzero = {0.0f,0.0f}; 创建新结构体所用的便捷的构造函数: cpVect cpv(const cpFloat x, const cpFloat y)
高德POI抓取 功能介绍 POI全称为Point of Interest,即兴趣点。在LSV的扩展插件中可以使用“高德POI抓取”功能来提取所选或者所绘制区域内中的所有包含自定义关键词的POI信息。 具体操作 在下载菜单中找到“高德POI抓取”功能,点击进入。 在绘制或选择所需提取POI的面后开始对关键词、POI类型进行设置,并且输入使用的高德KEY(详细
我试着用升压::变体 但我收到错误:错误:从'std::向量'转换为非标量类型'std::向量 编辑: 我在。h文件 现在我有以下错误: /softs/boost/1 . 53 . 0/64/gcc/4 . 5 . 1/include/boost/detail/reference _ content . HPP:在成员函数' void boost::detail::variant::assign