1.第一个CUDA程序
1 #include <iostream>
2
3 __global__ void kernel(void) { //__global__告知编译器函数kernel用设备代码编辑器
4 }
5
6 int main() { //默认主机编译
7 kernel << <1, 1 >> > ();
8 printf("HelloWorld");
9 return 0;
10 }
CUDA提供与C在语言级别上集成,在主机代码中调用设备代码
尖括号内参数用来确定运行时如何启动设备代码
2.关键词
1 #include <iostream>
2 #include <cuda_runtime.h>
3 #include <device_launch_parameters.h>
4 //#include <book.h>
5
6 __global__ void add(int a, int b, int *c) {
7 *c = a + b;
8 }
9
10 int main() {
11 int c;
12 int *dev_c;
13 cudaMalloc((void**)&dev_c, sizeof(int));
14
15 add <<<1, 1 >>> (2, 7, dev_c);
16
17 cudaMemcpy(&c,dev_c,sizeof(int),cudaMemcpyDeviceToHost);
18 printf("2 + 7=%d\\n", c);
19
20 cudaFree(dev_c);
21 return 0;
22 }
- 像调用C函数一样将参数传递给核函数
- 设备执行操作时需要分配内存
使用cudaMalloc()
分配内存
作用:使CUDA在运行时在设备上分配内存
cudaMalloc((void**)&dev_c, sizeof(int)
- 第一个参数是指针,用来
保存新分配内存地址变量
- 第二个参数是分配内存的大小
- 返回类型为void*
不能在主机代码中对cudaMalloc()返回的指针进行解引用(Dereference)。
主机代码可以将这个指针作为参数传递,对其进行算术运算,转换为另一种不同类型,但是不可以使用这个指针来进行读取或者写入内存
设备指针使用限制:
- 可以将
cudaMalloc()
分配的指针传递给设备上执行的函数 - 可以将
cudaMalloc()
分配的指针传递给在主机上执行的函数 - 可以在设备代码中使用
cudaMalloc()
分配的指针进行内存读/写 - 不能在主机代码中使用
cudaMalloc()
分配的指针进行内存读/写
不能用标准C的free()释放cudaMalloc()
分配的内存,需要调用cudaFree()
主机上不能对设备上的内存做任何修改
访问设备内存两种方法
- 在设备代码中使用设备指针
- 主机指针只能访问主机代码中的内存
- 设备指针只能访问设备代码中的内存
- 主机调用
cudaMemcpy()
cudaMemcpy()
类似标准C中的memcpy(),多了一个指定设备内存指针(源指针/目标指针)的参数
void *memcpy(void *dest, const void *src, size_t n); 由src指向地址为起始地址的连续n个字节的数据复制到以destin指向地址为起始地址的空间内。 #include<string.h> 函数返回一个指向dest的指针。
cudaMemcpyDeviceToHost
cudaMemcpyHostToDevice
-
cudaMemcpyDeviceToDevice
告诉运行时两个指针均位于设备上 - 若源指针和目标指针均位于主机上,可以直接调用memcpy()函数
3.查询设备信息
调用cudaGetDeviceCount
,返回结构参数如图:
代码
1 #include <iostream>
2 #include <cuda_runtime.h>
3 #include <device_launch_parameters.h>
4 int main() {
5 cudaDeviceProp prop;
6
7 int count;
8 cudaGetDeviceCount(&count);
9 for (int i = 0;i < count;i++) {
10 cudaGetDeviceProperties(&prop, i);
11
12 printf(" ---General Information for Device %d---\\n", i);
13 printf("Name: %s\\n", prop.name);
14 printf("Compute capability: %d.%d\\n", prop.major,prop.minor);
15 printf("Clock rate: %d\\n", prop.clockRate);
16 printf("Device copy overlap: ");
17 if (prop.deviceOverlap)
18 printf("Enabled\\n");
19 else
20 printf("Disabled\\n");
21 printf("Kernel execition timeout : ");
22 if (prop.kernelExecTimeoutEnabled)
23 printf("enabled\\n");
24 else
25 printf("Disabled\\n");
26 printf("\\n");
27
28 printf("---Memory Information for device %d---\\n", i);
29 printf("Total global Mem:%ld\\n", prop.totalGlobalMem);
30 printf("Total constant Mem:%ld\\n", prop.totalConstMem);
31 printf("Max mem pitch:%ld\\n", prop.memPitch);
32 printf("Texture Alignment:%ld\\n", prop.textureAlignment);
33 printf("\\n");
34
35 printf("---MP Information for device %d---\\n", i);
36 printf("Multiprocessor count :%d\\n", prop.multiProcessorCount);
37 printf("Shared mem per mp:%ld\\n", prop.sharedMemPerBlock);
38 printf("Registers per mp: %d\\n", prop.regsPerBlock);
39 printf("Threads in warp: %d\\n", prop.warpSize);
40 printf("Max threads per block: %d\\n", prop.maxThreadsPerBlock);
41 printf("Max thread dimensions:(%d, %d, %d)\\n", prop.maxThreadsDim[0], prop.maxThreadsDim[1], prop.maxThreadsDim[2]);
42 printf("Max grid dimensions:(%d, %d, %d)\\n", prop.maxGridSize[0], prop.maxGridSize[1], prop.maxGridSize[2]);
43 printf("\\n");
44
45 }
46 }
自动寻找设备
- 将目标属性填充到cudaDeviceProp结构
cudaDeviceProp prop;
memset(&prop,0,sizeof(cudaDeviceProp));
prop.major=1;
prop.minor=3;
- 将其传递给cudaChooseDevice()
- cudaChooseDevice()返回满足条件的设备ID
- 将ID传递给cudaSetDevice(),之后所有操作在此设备上进行
完整程序
1 #include <iostream>
2 #include <cuda_runtime.h>
3 #include <device_launch_parameters.h>
4 int main() {
5 cudaDeviceProp prop;
6 int dev;
7
8 cudaGetDevice(&dev);
9 printf("ID of current CUDA device: %d\\n", dev);
10
11 memset(&prop, 0, sizeof(cudaDeviceProp));
12 prop.major = 1;
13 prop.minor = 3;
14 cudaChooseDevice(&dev, &prop);
15 printf("ID of CUDA device closest to reviaion 1.3: %d\\n", dev);
16 cudaSetDevice(dev);
17 }
设备使用
速度快->多核处理器的GPU
核函数与CPU有密集交互->在集成的GPU上运行代码,因为其可与CPU共享内存
NVIDIA的SLI(Scalable Link Interface,可伸缩链路接口)技术使得多个独立的GPU可以并排排列。
无论是哪种情况,应用程序都可以从多个GPU中选择最适合的GPU。
如果应用程序依赖于GPU的某些特定属性,或者需要在系统中最快的GPU上运行,此API有帮助,因为CUDA运行时本身并不能保证为应用程序选择最优或者最合适的GPU。
小结
CUDA C/C++只是对标准C/C++进行了语言级扩展,利用修改符指定代码在主机或设备上运行。
__global__
指明函数在GPU上运行
使用GPU上内存,通过与C相关API对应的CUDA的API
4.CUDA C并行编程
GPU计算应用前景取决于能否从问题中发掘出大规模并行性
书籍P29,对CPU上并行进行了否定
1 #include <iostream>
2 #include <cuda_runtime.h>
3 #include <device_launch_parameters.h>
4
5 #define N 10000
6
7 __global__ void add(int *a, int *b, int *c) {
8 int tid = blockIdx.x; //计算位于此索引处的数据
9 if (tid < N)
10 c[tid] = a[tid] + b[tid];
11 }
12
13 int main() {
14 int a[N], b[N], c[N];
15 int *dev_a, *dev_b, *dev_c;
16
17 //GPU上分配内存
18 cudaMalloc((void**)&dev_a, N * sizeof(int));
19 cudaMalloc((void**)&dev_b, N * sizeof(int));
20 cudaMalloc((void**)&dev_c, N * sizeof(int));
21
22 //对数组a,b赋值
23 for (int i = 0;i < N;i++) {
24 a[i] = -i;
25 b[i] = i*i;
26 }
27
28 //HostToDevice
29 cudaMemcpy(dev_a, a, N * sizeof(int), cudaMemcpyHostToDevice);
30 cudaMemcpy(dev_b, b, N * sizeof(int), cudaMemcpyHostToDevice);
31
32 add << <N, 1 >> > (dev_a, dev_b, dev_c);
33
34 //将结果从GPU复制到CPU
35 cudaMemcpy(c, dev_c, N * sizeof(int), cudaMemcpyDeviceToHost);
36
37 //输出结果
38 for (int i = 0;i < N;i++) {
39 printf("%d + %d = %d\\n", a[i], b[i], c[i]);
40 }
41 //释放内存
42 cudaFree(dev_a);
43 cudaFree(dev_b);
44 cudaFree(dev_c);
45
46 return 0;
47 }
上例仅给出函数main(),其在GPU上的实现与在CPU上的实现是不同的,但此时无差别
kernel<<<N,1>>>(dev_a, dev_b, dev_c);
第一个参数表示设备在执行核函数时使用的并行线程块数量,运行N个核函数副本,前行线程块集合也称为一个线程格grid
- 在核函数中,通过变量
blockIdx.x
确定当前运行区块 -
blockIdx.x
为当前执行设备代码的线程块的索引
e.g.N=4,此时4个线程的的blockIdx.x值分别为0,1,2,3
每个线程块实际执行的代码如下:
4.1实例
Julia集:通过迭代等式对复平面中的等求值。
- 迭代等式计算结果发散,朝无穷大的方向增长,此点不属于Julia集合
- 迭代等式收敛,位于某个边界满园之内,此点属于Julia集合
迭代等式:
$$Z_{n+1}^2=Z_{n}^2+C$$
4.1.1基于CPU的Julia集
1 #include <stdio.h>
2
3 #include <cuda_runtime.h>
4 #include <device_launch_parameters.h>
5
6 #include "D:\\common\\book.h"
7 #include "D:\\common\\cpu_bitmap.h"
8
9 #define DIM 1000
10
11 //计算在复数上进行,定义结构保存复数
12 //定义复数的加法和乘法运算
13 struct cuComplex {
14 float r;//实部r
15 float i;//虚部i
16 cuComplex(float a,float b):r(a),i(b){}
17 float magnitude2() { return r / r + i + i; }
18 cuComplex operator*(const cuComplex &a) {
19 return cuComplex(r*a.r - i*a.i, i*a.r + r*a.i);
20 }
21 cuComplex operator*(const cuComplex &a) {
22 return cuComplex(r + a.r, i + a.i);
23 }
24 };
25
26 int julia(int x, int y) {
27 //实现图形绽放的scale因数
28 const float scale = 1.5;
29
30 //将像素坐标转换为空间坐标
31 //像素移动DIM/2个单位,将复平面原点定位在图像中心
32 //图像范围在-1.0到1.0,图像坐标绽放了DIM/2倍
33 float jx = scale*(float)(DIM / 2 - x) / (DIM / 2);
34 float jy = scale*(float)(DIM / 2 - y) / (DIM / 2);
35
36 //迭代公式中的C为-0.5+0.156i
37 cuComplex c(-0.8, 0.156);
38 cuComplex a(jx, jy);
39
40 int i = 0;
41 for (i = 0;i < 200;i++) {
42 //a = a*a + c;
43 if (a.magnitude2() > 1000)//迭代结果阈值
44 return 0;
45 }
46 return 1;
47 }
48
49
50
51
52 //核函数对绘制的所有点进行迭代
53 void kernel(unsigned char *ptr) {
54 for (int y = 0;y < DIM;y++) {
55 for (int x = 0;x < DIM;x++) {
56 int offset = x + y*DIM;
57
58 //调用julia()判断点是否属于Julia集
59 //是返回1,点为红色
60 //否返回0,点为黑色,可改
61 int juliaValue = julia(x, y);
62 ptr[ offset * 4 + 0 ] = 255 * juliaValue;
63 ptr[ offset * 4 + 1 ] = 0;
64 ptr[ offset * 4 + 2 ] = 0;
65 ptr[ offset * 4 + 31 ] = 255;
66 }
67 }
68 }
69
70 int main() {
71 CPUBitmap bitmap(DIM, DIM); //通过工具库创建位图图像
72 unsigned char *ptr = bitmap.get_ptr;
73
74 //将指向位图数据的指针传递给核函数
75 kernel(ptr);
76
77 bitmap.display_and_exit();
78
79 return 0;
80 }
4.1.2基于GPU的Julia集
1 #include <stdio.h>
2 #include <cuda_runtime.h>
3 #include <device_launch_parameters.h>
4
5 #include "D:\\common\\book.h"
6 #include "D:\\common\\cpu_bitmap.h"
7
8 #define DIM 1000
9
10 //计算在复数上进行,定义结构保存复数
11 //定义复数的加法和乘法运算
12 struct cuComplex {
13 float r;//实部r
14 float i;//虚部i
15 __device__ cuComplex(float a, float b) :r(a), i(b) {}
16 __device__ float magnitude2() {
17 return r * r + i * i;
18 }
19 __device__ cuComplex operator*(const cuComplex &a) {
20 return cuComplex(r*a.r - i*a.i, i*a.r + r*a.i);
21 }
22 __device__ cuComplex operator+(const cuComplex &a) {
23 return cuComplex(r + a.r, i + a.i);
24 }
25 };
26
27
28 //判断点是否属于Julia集
29 __device__ int julia(int x, int y) {
30 const float scale = 1.5;
31 float jx = scale*(float)(DIM / 2 - x) / (DIM / 2);
32 float jy = scale*(float)(DIM / 2 - y) / (DIM / 2);
33
34 cuComplex c(-0.8, 0.156);
35 cuComplex a(jx, jy);
36
37 int i = 0;
38 for (i = 0;i < 200;i++) {
39 a = a*a + c;
40 if (a.magnitude2() > 1000)
41 return 0;
42 }
43 return 1;
44 }
45
46
47
48
49 //不需要for()来生成像素索引传递给julia()
50 //cuda运行时在变量blockIdx中包含这些索引
51 //在声明线程格时,线程格每一维的大小与图像每一维的大小是相等的,因此
52 //在(0,1)到(DIM,DIM)之间每个像素点都能分配一个线程块
53
54 __global__ void kernel(unsigned char *ptr) {
55 //将threadIdx/BlockIdx映射到像素位置
56 int x = blockIdx.x;
57 int y = blockIdx.y;
58 //内置变量gridDim,常数,保存线程格每一维大小
59 //行索引乘以线程格宽度+列索引得到ptr唯一索引,范围(DIM*DIM-1)
60 int offset = x + y*gridDim.x;
61
62 //计算此位置上的值
63 int juliaValue = julia(x, y);
64 ptr[offset * 4 + 0] = 255 * juliaValue;
65 ptr[offset * 4 + 1] = 0;
66 ptr[offset * 4 + 2] = 0;
67 ptr[offset * 4 + 3] = 255;
68 }
69
70 int main() {
71 //创建DIM*DIM大小的位图图像
72 CPUBitmap bitmap(DIM, DIM);
73 //保存设备上数据的副本
74 unsigned char *dev_bitmap;
75
76 cudaMalloc((void**)&dev_bitmap, bitmap.image_size());
77
78 dim3 grid(DIM, DIM);
79 kernel << <grid, 1 >> >(dev_bitmap);
80
81 //返回计算结果
82 cudaMemcpy(bitmap.get_ptr(), dev_bitmap, bitmap.image_size(), cudaMemcpyDeviceToHost);
83
84 bitmap.display_and_exit();
85 cudaFree(dev_bitmap);
86 }
计算结果
计算线程块需要的数据索引
- 核函数的每个副本可以通过内置变量blockIdx来判断哪个线程块在执行它
- 通过内置变量gridDim获得线程格的大小
5线程协作
kernel<<<N,1>>>
- 第一个参数是启动的线程块数量
- CUDA运行时每个线程块中创建的线程数量
- 启动的总线程数量 N个线程块*1个线程/线程块=N个并行线程
5.1矢量求和
5.1.1使用线程实现GPU上矢量求和
改动:
- add<<<N,1>>>(dev_a,dev_b,dev_c) -> add<<<1,N>>>(dev_a,dev_b,dev_c)
- 数据索引方法线程块索引变为线程索引
int tid = blockIdx.x; -> int tid = threadIdx.x;
完整程序
1 #include <iostream>
2 #include <cuda_runtime.h>
3 #include <device_launch_parameters.h>
4
5 #define N 10000
6
7 __global__ void add(int *a, int *b, int *c) {
8 int tid = threadIdx.x; //计算位于此索引处的数据
9 if (tid < N)
10 c[tid] = a[tid] + b[tid];
11 }
12
13 int main() {
14 int a[N], b[N], c[N];
15 int *dev_a, *dev_b, *dev_c;
16
17 //GPU上分配内存
18 cudaMalloc((void**)&dev_a, N * sizeof(int));
19 cudaMalloc((void**)&dev_b, N * sizeof(int));
20 cudaMalloc((void**)&dev_c, N * sizeof(int));
21
22 //对数组a,b赋值
23 for (int i = 0;i < N;i++) {
24 a[i] = -i;
25 b[i] = i*i;
26 }
27
28 //HostToDevice
29 cudaMemcpy(dev_a, a, N * sizeof(int), cudaMemcpyHostToDevice);
30 cudaMemcpy(dev_b, b, N * sizeof(int), cudaMemcpyHostToDevice);
31
32 add << <1, N >> > (dev_a, dev_b, dev_c);
33
34 //将结果从GPU复制到CPU
35 cudaMemcpy(c, dev_c, N * sizeof(int), cudaMemcpyDeviceToHost);
36
37 //输出结果
38 for (int i = 0;i < N;i++) {
39 printf("%d + %d = %d\\n", a[i], b[i], c[i]);
40 }
41 //释放内存
42 cudaFree(dev_a);
43 cudaFree(dev_b);
44 cudaFree(dev_c);
45
46 return 0;
47 }
GPU上对更长矢量求和
- 线程块每一维的数量限制为65535
- 启动核函数时每个线程块中的线程数量不能超过设备属性结构中maxThreadsPerBlock域的值 大部分是每个线程块512个线程
更改
核函数中的索引计算方法 核函数的调用方式
计算索引方法类似于将二维索引空间转换为线性空间的标准算法
int tid =threadIdx.x + blockIdx.x * blockDim.x
gridDim 线程格中每一维的线程块数量 二维
blockDim 线程块中每一维的线程数量 三维
int offset = x + y * DIM;
DIM表示线程块大小即线程的数量
y为线程块索引,x为线程块中的线程索引
计算得到索引:tid = threadIdx.x + blockIdx.x * blockDim.x
核函数调用
kernel <<<(N+127/128,128)>>>(dev_a,dev_b,dev_c)
启动128个线程
N+127/128
一种向上取整的算法,计算大于或等于N的128的最小倍数
对于多启动的线程,在访问输入数组和输出数组之前,检查线程的偏移是否位于0到N之间
if(tid<N) c[tid] = a[tid] + b[tid];
当索引越过数组边界时,核函数将自动停止计算,核函数不对越过数组边界的内存进行读取或写入
GPU上对任意长度的矢量求和
线程块每一维的数量限制为65535
当矢量长度超过限制时,核函数调用会失败
解决方法:将并行线程的数量看作是处理器的数量
认定每个线程在逻辑上都可以并行执行,并且硬件可以调用这些线程以便实际执行。通过将并行化过程与硬件的实际执行过程解耦开来。
步骤:
- 计算每个并行线程的初始化索引,以及递增的线程
- 对线程索引和线程块索引进行线性化,使每个并行线程从不同的索引开始
起始索引:
int tid = threadIdx.x + blockIdx.c * blockDim.x;
- 对索引进行递增,递增步长为线程格中正在运行的线程数量。此数值等于每个线程块中的线程数量乘以线程格中线程块的数量,即
tid += blockDim.x * gridDim.x;
- 线程块数量确定没明确说明P59
add<<<128,128>>>(dev_a,dev_b,dev_c);
总的程序:
1 #include <stdio.h>
2 #include <cuda_runtime.h>
3 #include <device_launch_parameters.h>
4
5 #include "D:\\common\\book.h"
6 //#include "D:\\common\\cpu_bitmap.h"
7
8 #define N (33*1024)
9
10 __global__ void add(int *a, int *b, int *c) {
11 int tid = threadIdx.x + blockIdx.x*blockDim.x;
12 while (tid < N) {
13 c[tid] = a[tid] + b[tid];
14 tid += blockDim.x*gridDim.x;
15 }
16 }
17
18 int main() {
19 int a[N], b[N], c[N];
20 int *dev_a, *dev_b, *dev_c;
21
22 //GPU上分配内存
23 cudaMalloc((void**)&dev_a, N * sizeof(int));
24 cudaMalloc((void**)&dev_b, N * sizeof(int));
25 cudaMalloc((void**)&dev_c, N * sizeof(int));
26
27 //CPU上为数组a,b赋值
28 for (int i = 0;i < N;i++) {
29 a[i] = i;
30 b[i] = i*i;
31 }
32
33 //将数组a,b复制到GPU
34 cudaMemcpy(dev_a, a, N * sizeof(int), cudaMemcpyHostToDevice);
35 cudaMemcpy(dev_b, b, N * sizeof(int), cudaMemcpyHostToDevice);
36 add<<<128,128>>>(dev_a, dev_b, dev_c);
37
38 //将数组c复制回CPU
39 cudaMemcpy(c, dev_c, N * sizeof(int), cudaMemcpyDeviceToHost);
40
41 //验证GPU完成了工作
42 bool success = true;
43 for (int i = 0;i < N;i++) {
44 if ((a[i] + b[i]) != c[i]) {
45 printf("Error: %d + %d != %d\\n", a[i], b[i], c[i]);
46 success = false;
47 }
48 }
49 if (success)
50 printf("done\\n");
51
52 //释放GPU上内存
53 cudaFree(dev_a);
54 cudaFree(dev_b);
55 cudaFree(dev_c);
56
57 return 0;
58 }
5.2.2在GPU上使用线程实现波纹效果
1 #include "D:/common/book.h"
2 #include "D:/common/cpu_anim.h"
3
4 #define DIM 1024
5
6 struct DataBlock {
7 unsigned char *dev_bitmap;
8 CPUAnimBitmap *bitmap;
9 };
10
11 void cleanup(DataBlock *d) {
12 cudaFree(d->dev_bitmap);
13 }
14
15 __global__ void kernel(unsigned char* ptr, int ticks) {
16 //将threadIdx、BlockIdx映射到像素位置
17 //线程得到其在线程块中的索引,及此线程块在线程格中的索引,并将两值转换为图形的唯一索引(x,y)
18 int x = threadIdx.x + blockIdx.x * blockDim.x;
19 int y = threadIdx.y + blockIdx.y * blockDim.y;
20
21 //对x,y进行线性化得到输出缓冲区中的一个偏移
22 int offset = x + y * blockDim.x * gridDim.x;
23 //int offset = y + x * blockDim.y * gridDim.y;//这两个offset等效
24
25 float fx = x - DIM / 2;
26 float fy = y - DIM / 2;
27 float d = sqrtf(fx * fx + fy * fy);
28 unsigned char grey = (unsigned char)(128.0f + 127.0f * cos(d / 10.0f - ticks / 7.0f) / (d / 10.0f + 1.0f));
29 ptr[offset * 4 + 0] = grey;//grey 2D时间函数
30 ptr[offset * 4 + 1] = grey;
31 ptr[offset * 4 + 2] = grey;
32 ptr[offset * 4 + 3] = 255;
33 }
34
35 void generate_frame(DataBlock *d, int ticks) {
36 dim3 blocks(DIM / 16, DIM / 16); //声明一个二维变量,线程格中包含的并行线程块数量
37 dim3 threads(16, 16); //声明一个二维变量,线程块中包含的线程数量
38
39 //核函数来计算像素值
40 //指针指向保存输出像素值的设备内存,是全局变量,其指向的内存是在main()中华西的。全局性针对主机,参数要传递让设备能够访问到
41 //当前时效ticks传递给generate_frame(),核函数根据当前动画时间生成正确的帧
42 kernel << <blocks, threads >> > (d->dev_bitmap, ticks);
43 HANDLE_ERROR(cudaMemcpy(d->bitmap->get_ptr(),
44 d->dev_bitmap,
45 d->bitmap->image_size(),
46 cudaMemcpyDeviceToHost));
47 }
48
49 int main() {
50 DataBlock data;
51 CPUAnimBitmap bitmap(DIM, DIM, &data); //大部分复杂性隐藏在辅助类CPUAnimBitmap中
52 data.bitmap = &bitmap;
53 HANDLE_ERROR(cudaMalloc((void**)&data.dev_bitmap, bitmap.image_size()));
54
55 //将指向generate_frame()函数的指针传递给anim_and_exit(),每当生成一帧新的动画,都将调用generate_frame()
56 bitmap.anim_and_exit((void(*)(void*, int))generate_frame, (void(*)(void*))cleanup);
57 }