Compute Library
 21.02
CpuElementwiseKernel.cpp
Go to the documentation of this file.
1 /*
2  * Copyright (c) 2018-2021 Arm Limited.
3  *
4  * SPDX-License-Identifier: MIT
5  *
6  * Permission is hereby granted, free of charge, to any person obtaining a copy
7  * of this software and associated documentation files (the "Software"), to
8  * deal in the Software without restriction, including without limitation the
9  * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
10  * sell copies of the Software, and to permit persons to whom the Software is
11  * furnished to do so, subject to the following conditions:
12  *
13  * The above copyright notice and this permission notice shall be included in all
14  * copies or substantial portions of the Software.
15  *
16  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22  * SOFTWARE.
23  */
25 
28 #include "src/core/CPP/Validate.h"
36 
37 #include <arm_neon.h>
38 
39 namespace arm_compute
40 {
41 namespace cpu
42 {
43 namespace kernels
44 {
45 namespace
46 {
47 using ElementwiseSelector = std::add_pointer<bool(DataType)>::type;
49 struct ElementwiseKernel
50 {
51  const char *name;
52  const ElementwiseSelector is_selected;
53  UKernelType *ukernel;
54 };
55 
56 template <DataType dt>
57 inline bool is_selected(DataType data_type)
58 {
59  return dt == data_type;
60 }
61 
62 template <DataType input_data_type, DataType output_data_type = input_data_type>
63 static ElementwiseKernel generate_kernel(UKernelType *ukernel)
64 {
65  std::string kernel_name("op_");
66  kernel_name += string_from_data_type(input_data_type) + "_";
67  kernel_name += string_from_data_type(input_data_type) + "_";
68  kernel_name += string_from_data_type(output_data_type);
69 
70  return { kernel_name.c_str(), is_selected<input_data_type>, ukernel };
71 }
72 
73 template <ArithmeticOperation op>
74 std::function<void(const ITensor *, const ITensor *, ITensor *, const Window &)>
75 configure_arithm_func(const ITensorInfo *input1, const ITensorInfo *input2, ITensorInfo *output)
76 {
77  ARM_COMPUTE_UNUSED(input2, output);
78  static ElementwiseKernel kernels[] =
79  {
80 #if defined(__ARM_FEATURE_SVE)
81  generate_kernel<DataType::F32>(REGISTER_FP32_SVE((arm_compute::cpu::sve::elementwise_arithmetic_op<op, float32_t>))),
82  generate_kernel<DataType::S32>(REGISTER_INTEGER_SVE((arm_compute::cpu::sve::elementwise_arithmetic_op<op, int32_t>))),
83 #else /* defined(__ARM_FEATURE_SVE) */
84  generate_kernel<DataType::F32>(REGISTER_FP32_NEON((arm_compute::cpu::elementwise_arithm_op<op, typename wrapper::traits::neon_vector<float, 4>>))),
85  generate_kernel<DataType::S32>(REGISTER_INTEGER_NEON((arm_compute::cpu::elementwise_arithm_op<op, typename wrapper::traits::neon_vector<int32_t, 4>>))),
86 #endif /* defined(__ARM_FEATURE_SVE) */
87 #if defined(__ARM_FEATURE_SVE2)
88  generate_kernel<DataType::QASYMM8>(REGISTER_QASYMM8_SVE((arm_compute::cpu::sve::elementwise_arithmetic_quantized_op<op, uint8_t>))),
89  generate_kernel<DataType::QASYMM8_SIGNED>(REGISTER_QASYMM8_SIGNED_SVE((arm_compute::cpu::sve::elementwise_arithmetic_quantized_op<op, int8_t>))),
90 #else /* defined(__ARM_FEATURE_SVE2) */
91  generate_kernel<DataType::QASYMM8>(REGISTER_QASYMM8_NEON((arm_compute::cpu::elementwise_arithm_op_quantized<op>))),
92  generate_kernel<DataType::QASYMM8_SIGNED>(REGISTER_QASYMM8_SIGNED_NEON((arm_compute::cpu::elementwise_arithm_op_quantized_signed<op>))),
93 #endif /* defined(__ARM_FEATURE_SVE2) */
94 #ifdef __ARM_FEATURE_FP16_VECTOR_ARITHMETIC
95 #if defined(__ARM_FEATURE_SVE)
96  generate_kernel<DataType::F16>(REGISTER_FP16_SVE((arm_compute::cpu::sve::elementwise_arithmetic_op<op, float16_t>))),
97 #else /* defined(__ARM_FEATURE_SVE) */
98  generate_kernel<DataType::F16>(REGISTER_FP16_NEON((arm_compute::cpu::elementwise_arithm_op<op, typename wrapper::traits::neon_vector<float16_t, 8>>))),
99 #endif /* defined(__ARM_FEATURE_SVE) */
100 #endif /* __ARM_FEATURE_FP16_VECTOR_ARITHMETIC */
101  generate_kernel<DataType::S16>(REGISTER_INTEGER_NEON((arm_compute::cpu::elementwise_arithm_op<op, typename wrapper::traits::neon_vector<int16_t, 8>>))),
102  };
103 
104  for(const auto &uk : kernels)
105  {
106  if(uk.is_selected(input1->data_type()))
107  {
108  return uk.ukernel;
109  }
110  }
111 
112  return nullptr;
113 }
114 
115 template <ComparisonOperation op>
116 std::function<void(const ITensor *input1, const ITensor *input2, ITensor *output, const Window &window)>
117 configure_comp_func(const ITensorInfo *input1, const ITensorInfo *input2, ITensorInfo *output)
118 {
119  ARM_COMPUTE_UNUSED(input2, output);
120  static ElementwiseKernel kernels[] =
121  {
122 #if defined(__ARM_FEATURE_SVE)
123  generate_kernel<DataType::U8, DataType::U8>(REGISTER_INTEGER_SVE((arm_compute::cpu::sve::elementwise_comparison_op<op, uint8_t>))),
124  generate_kernel<DataType::F32, DataType::U8>(REGISTER_FP32_SVE((arm_compute::cpu::sve::elementwise_comparison_op<op, float>))),
125  generate_kernel<DataType::S16, DataType::U8>(REGISTER_INTEGER_SVE((arm_compute::cpu::sve::elementwise_comparison_op<op, int16_t>))),
126  generate_kernel<DataType::S32, DataType::U8>(REGISTER_INTEGER_SVE((arm_compute::cpu::sve::elementwise_comparison_op<op, int32_t>))),
127 #else /* defined(__ARM_FEATURE_SVE) */
128  generate_kernel<DataType::U8, DataType::U8>(REGISTER_INTEGER_NEON((arm_compute::cpu::elementwise_comp_op_8<op, uint8_t, uint8x16_t>))),
129  generate_kernel<DataType::F32, DataType::U8>(REGISTER_FP32_NEON((arm_compute::cpu::elementwise_comp_op_32<op, float, float32x4_t>))),
130  generate_kernel<DataType::S16, DataType::U8>(REGISTER_INTEGER_NEON((arm_compute::cpu::elementwise_comp_op_16<op, int16_t, int16x8_t>))),
131  generate_kernel<DataType::S32, DataType::U8>(REGISTER_INTEGER_NEON((arm_compute::cpu::elementwise_comp_op_32<op, int32_t, int32x4_t>))),
132 #endif /* defined(__ARM_FEATURE_SVE) */
133 #if defined(__ARM_FEATURE_SVE2)
134  generate_kernel<DataType::QASYMM8_SIGNED, DataType::U8>(REGISTER_QASYMM8_SIGNED_SVE((arm_compute::cpu::sve::elementwise_comparison_quantized_op<op, int8_t>))),
135  generate_kernel<DataType::QASYMM8, DataType::U8>(REGISTER_QASYMM8_SVE((arm_compute::cpu::sve::elementwise_comparison_quantized_op<op, uint8_t>))),
136 #else /* defined(__ARM_FEATURE_SVE2) */
137  generate_kernel<DataType::QASYMM8_SIGNED, DataType::U8>(REGISTER_QASYMM8_SIGNED_NEON((arm_compute::cpu::elementwise_comp_op_quantized_signed<op>))),
138  generate_kernel<DataType::QASYMM8, DataType::U8>(REGISTER_QASYMM8_NEON((arm_compute::cpu::elementwise_comp_op_quantized<op>))),
139 #endif /* defined(__ARM_FEATURE_SVE2) */
140 #ifdef __ARM_FEATURE_FP16_VECTOR_ARITHMETIC
141 #if defined(__ARM_FEATURE_SVE)
142  generate_kernel<DataType::F16, DataType::U8>(REGISTER_FP16_SVE((arm_compute::cpu::sve::elementwise_comparison_op<op, float16_t>))),
143 #else /* defined(__ARM_FEATURE_SVE) */
144  generate_kernel<DataType::F16, DataType::U8>(REGISTER_FP16_NEON((arm_compute::cpu::elementwise_comp_op_16<op, float16_t, float16x8_t>))),
145 #endif /* defined(__ARM_FEATURE_SVE) */
146 #endif /* __ARM_FEATURE_FP16_VECTOR_ARITHMETIC */
147  };
148 
149  for(const auto &uk : kernels)
150  {
151  if(uk.is_selected(input1->data_type()))
152  {
153  return uk.ukernel;
154  }
155  }
156 
157  return nullptr;
158 }
159 } // namespace
160 
161 Status CpuElementwiseKernel::validate_arguments_common(const ITensorInfo &input1, const ITensorInfo &input2, const ITensorInfo &output)
162 {
165 
166  const TensorShape out_shape = TensorShape::broadcast_shape(input1.tensor_shape(), input2.tensor_shape());
167 
168  ARM_COMPUTE_RETURN_ERROR_ON_MSG(out_shape.total_size() == 0, "Inputs are not broadcast compatible");
169 
170  // Validate in case of configured output
171  if(output.total_size() > 0)
172  {
173  ARM_COMPUTE_RETURN_ERROR_ON_MSG(detail::have_different_dimensions(out_shape, output.tensor_shape(), 0),
174  "Wrong shape for output");
175  }
176 
177  return Status{};
178 }
179 
180 void CpuElementwiseKernel::configure_common(const ITensorInfo *input1, const ITensorInfo *input2, ITensorInfo *output)
181 {
182  ARM_COMPUTE_ERROR_ON_NULLPTR(input1, input2, output);
183 
184  // Configure kernel window
185  const std::pair<TensorShape, ValidRegion> broadcast_pair = ITensorInfo::broadcast_shape_and_valid_region(*input1, *input2);
186  const TensorShape &out_shape = broadcast_pair.first;
187  const ValidRegion &valid_region = broadcast_pair.second;
188 
189  // Auto initialize output if not initialized
190  auto_init_if_empty(*output, out_shape, 1, input1->data_type());
191 
192  Window win = calculate_max_window(valid_region);
193 
194  ICpuKernel::configure(win);
195 }
196 
197 void CpuElementwiseKernel::run_op(ITensorPack &tensors, const Window &window, const ThreadInfo &info)
198 {
199  ARM_COMPUTE_UNUSED(info, window);
202 
203  auto src0 = tensors.get_const_tensor(TensorType::ACL_SRC_0);
204  auto src1 = tensors.get_const_tensor(TensorType::ACL_SRC_1);
205  auto dst = tensors.get_tensor(TensorType::ACL_DST);
206 
207  auto function = get_implementation(src0->info(), src1->info(), dst->info());
208  ARM_COMPUTE_ERROR_ON(function == nullptr);
209  function(src0, src1, dst, window);
210 }
211 
212 /** Arithmetic operators (min, max, squared_diff) */
214 {
215  ARM_COMPUTE_ERROR_THROW_ON(validate_arguments(*input1, *input2, *output));
216  configure_common(input1, input2, output);
217  _op = op;
218 }
219 
220 Status CpuArithmeticKernel::validate_arguments(const ITensorInfo &input1, const ITensorInfo &input2, const ITensorInfo &output)
221 {
223  // Validate in case of configured output
224  if(output.total_size() > 0)
225  {
227  }
228  return validate_arguments_common(input1, input2, output);
229 }
230 
232 {
233  ARM_COMPUTE_UNUSED(op);
234  ARM_COMPUTE_RETURN_ERROR_ON_NULLPTR(input1, input2, output);
235  ARM_COMPUTE_RETURN_ON_ERROR(validate_arguments(*input1, *input2, *output));
236  return Status{};
237 }
238 
239 std::function<CpuElementwiseKernel::ElementwiseFunction>
240 CpuArithmeticKernel::get_implementation(const ITensorInfo *input1, const ITensorInfo *input2, ITensorInfo *output)
241 {
242  switch(_op)
243  {
245  return configure_arithm_func<ArithmeticOperation::MAX>(input1, input2, output);
247  return configure_arithm_func<ArithmeticOperation::MIN>(input1, input2, output);
249  return configure_arithm_func<ArithmeticOperation::SQUARED_DIFF>(input1, input2, output);
251  return configure_arithm_func<ArithmeticOperation::PRELU>(input1, input2, output);
253  return configure_arithm_func<ArithmeticOperation::DIV>(input1, input2, output);
255  return configure_arithm_func<ArithmeticOperation::POWER>(input1, input2, output);
256  default:
257  ARM_COMPUTE_ERROR("NOT_SUPPORTED!");
258  }
259  return nullptr;
260 }
261 
262 /** The division operator */
263 
264 void CpuDivisionKernel::configure(const ITensorInfo *input1, const ITensorInfo *input2, ITensorInfo *output)
265 {
266  ARM_COMPUTE_ERROR_THROW_ON(validate_arguments(*input1, *input2, *output));
267  configure_common(input1, input2, output);
269 }
270 
271 Status CpuDivisionKernel::validate_arguments(const ITensorInfo &input1, const ITensorInfo &input2, const ITensorInfo &output)
272 {
274  return CpuArithmeticKernel::validate_arguments(input1, input2, output);
275 }
276 
277 Status CpuDivisionKernel::validate(const ITensorInfo *input1, const ITensorInfo *input2, const ITensorInfo *output)
278 {
279  ARM_COMPUTE_RETURN_ERROR_ON_NULLPTR(input1, input2, output);
280  ARM_COMPUTE_RETURN_ON_ERROR(validate_arguments(*input1, *input2, *output));
281  return Status{};
282 }
283 
284 /** The power operator */
285 void CpuPowerKernel::configure(const ITensorInfo *input1, const ITensorInfo *input2, ITensorInfo *output)
286 {
287  ARM_COMPUTE_ERROR_THROW_ON(validate_arguments(*input1, *input2, *output));
288  configure_common(input1, input2, output);
290 }
291 
292 Status CpuPowerKernel::validate_arguments(const ITensorInfo &input1, const ITensorInfo &input2, const ITensorInfo &output)
293 {
295  return CpuArithmeticKernel::validate_arguments(input1, input2, output);
296 }
297 
298 Status CpuPowerKernel::validate(const ITensorInfo *input1, const ITensorInfo *input2, const ITensorInfo *output)
299 {
300  ARM_COMPUTE_RETURN_ERROR_ON_NULLPTR(input1, input2, output);
301  ARM_COMPUTE_RETURN_ON_ERROR(validate_arguments(*input1, *input2, *output));
302  return Status{};
303 }
304 
305 /** Comparison operators (equal, not equal, less than, greater than, less than or equal, greater than or equal) */
307 {
308  ARM_COMPUTE_ERROR_THROW_ON(validate_arguments(*input1, *input2, *output));
309  configure_common(input1, input2, output);
310  _op = op;
311 }
312 
313 Status CpuComparisonKernel::validate_arguments(const ITensorInfo &input1, const ITensorInfo &input2, const ITensorInfo &output)
314 {
316  // Validate in case of configured output
317  if(output.total_size() > 0)
318  {
320  }
321  return validate_arguments_common(input1, input2, output);
322 }
323 
325 {
326  ARM_COMPUTE_UNUSED(op);
327  ARM_COMPUTE_RETURN_ERROR_ON_NULLPTR(input1, input2, output);
328  ARM_COMPUTE_RETURN_ON_ERROR(validate_arguments(*input1, *input2, *output));
329  return Status{};
330 }
331 
332 std::function<CpuElementwiseKernel::ElementwiseFunction>
333 CpuComparisonKernel::get_implementation(const ITensorInfo *input1, const ITensorInfo *input2, ITensorInfo *output)
334 {
335  switch(_op)
336  {
338  return configure_comp_func<ComparisonOperation::Equal>(input1, input2, output);
340  return configure_comp_func<ComparisonOperation::NotEqual>(input1, input2, output);
342  return configure_comp_func<ComparisonOperation::Greater>(input1, input2, output);
344  return configure_comp_func<ComparisonOperation::GreaterEqual>(input1, input2, output);
346  return configure_comp_func<ComparisonOperation::Less>(input1, input2, output);
348  return configure_comp_func<ComparisonOperation::LessEqual>(input1, input2, output);
349  default:
350  ARM_COMPUTE_ERROR("NOT_SUPPORTED!");
351  }
352  return nullptr;
353 }
354 } // namespace kernels
355 } // namespace cpu
356 } // namespace arm_compute
static Status validate(const ITensorInfo *input1, const ITensorInfo *input2, const ITensorInfo *output)
Static function to check if given info will lead to a valid configuration of CpuPowerKernel.
Window calculate_max_window(const ValidRegion &valid_region, const Steps &steps, bool skip_border, BorderSize border_size)
ArithmeticOperation
Available element-wise operations.
Definition: Types.h:534
const Window & window() const
The maximum window the kernel can be executed on.
Definition: IKernel.cpp:28
#define ARM_COMPUTE_RETURN_ERROR_ON_CPU_F16_UNSUPPORTED(tensor)
Definition: Validate.h:108
#define REGISTER_FP16_NEON(func_name)
Definition: Registrars.h:42
void configure(const ITensorInfo *input1, const ITensorInfo *input2, ITensorInfo *output)
Configure kernel.
#define ARM_COMPUTE_ERROR(msg)
Print the given message then throw an std::runtime_error.
Definition: Error.h:352
1 channel, 1 U8 per channel
#define REGISTER_FP32_NEON(func_name)
Definition: Registrars.h:52
#define ARM_COMPUTE_RETURN_ON_ERROR(status)
Checks if a status contains an error and returns it.
Definition: Error.h:204
1 channel, 1 F32 per channel
#define REGISTER_FP32_SVE(func_name)
Definition: Registrars.h:53
static TensorShape broadcast_shape(const Shapes &... shapes)
If shapes are broadcast compatible, return the broadcasted shape.
Definition: TensorShape.h:211
#define REGISTER_QASYMM8_SVE(func_name)
Definition: Registrars.h:73
void configure(const ITensorInfo *input1, const ITensorInfo *input2, ITensorInfo *output)
Configure kernel.
#define ARM_COMPUTE_ERROR_ON(cond)
If the condition is true then an error message is printed and an exception thrown.
Definition: Error.h:466
#define REGISTER_QASYMM8_SIGNED_NEON(func_name)
Definition: Registrars.h:62
Store the tensor&#39;s metadata.
Definition: ITensorInfo.h:40
#define ARM_COMPUTE_ERROR_THROW_ON(status)
Definition: Error.h:455
Status class.
Definition: Error.h:52
const ValidRegion valid_region
Definition: Scale.cpp:221
static std::pair< TensorShape, ValidRegion > broadcast_shape_and_valid_region(const Infos &... infos)
If infos are broadcast compatible tensor info&#39;s, return the broadcasted shape and the intersection of...
Definition: ITensorInfo.h:271
decltype(strategy::transforms) typedef type
void configure(ComparisonOperation op, const ITensorInfo *input1, const ITensorInfo *input2, ITensorInfo *output)
Configure kernel.
Copyright (c) 2017-2021 Arm Limited.
1 channel, 1 F16 per channel
#define REGISTER_INTEGER_NEON(func_name)
Definition: Registrars.h:92
DataType dt
#define ARM_COMPUTE_RETURN_ERROR_ON_NULLPTR(...)
Definition: Validate.h:163
#define REGISTER_QASYMM8_SIGNED_SVE(func_name)
Definition: Registrars.h:63
1 channel, 1 S32 per channel
void run_op(ITensorPack &tensors, const Window &window, const ThreadInfo &info) override
Execute the kernel on the passed window.
const DataType data_type
Definition: Im2Col.cpp:150
VectorType::type elementwise_arithm_op(const typename VectorType::type &a, const typename VectorType::type &b)
const ITensor * get_const_tensor(int id) const
Get constant tensor of a given id.
Definition: ITensorPack.cpp:40
const std::string & string_from_data_type(DataType dt)
Convert a data type identity into a string.
Definition: Utils.cpp:135
#define ARM_COMPUTE_UNUSED(...)
To avoid unused variables warnings.
Definition: Error.h:152
#define REGISTER_QASYMM8_NEON(func_name)
Definition: Registrars.h:72
quantized, asymmetric fixed-point 8-bit number unsigned
std::string kernel_name
#define REGISTER_INTEGER_SVE(func_name)
Definition: Registrars.h:93
bool auto_init_if_empty(ITensorInfo &info, const TensorShape &shape, int num_channels, DataType data_type, QuantizationInfo quantization_info=QuantizationInfo())
Auto initialize the tensor info (shape, number of channels and data type) if the current assignment i...
bool have_different_dimensions(const Dimensions< T > &dim1, const Dimensions< T > &dim2, unsigned int upper_dim)
Definition: Validate.h:51
const ElementwiseSelector is_selected
ComparisonOperation
Supported comparison operations.
Definition: Types.h:177
y*x if x < 0, x otherwise
#define ARM_COMPUTE_ERROR_ON_UNCONFIGURED_KERNEL(k)
Definition: Validate.h:941
1 channel, 1 S16 per channel
static Status validate(ComparisonOperation op, const ITensorInfo *input1, const ITensorInfo *input2, const ITensorInfo *output)
Static function to check if given info will lead to a valid configuration of cpu::kernels::CpuCompari...
ScaleKernelInfo info(interpolation_policy, default_border_mode, PixelValue(), sampling_policy, false)
ITensor * get_tensor(int id)
Get tensor of a given id from the pac.
Definition: ITensorPack.cpp:50
Information about executing thread and CPU.
Definition: CPPTypes.h:235
virtual size_t total_size() const =0
Returns the total size of the tensor in bytes.
#define REGISTER_FP16_SVE(func_name)
Definition: Registrars.h:43
void(const ITensor *, const ITensor *, ITensor *, const Window &) ElementwiseFunction
Common signature for all the specialised arithmetic functions.
#define ARM_COMPUTE_RETURN_ERROR_ON_MISMATCHING_DATA_TYPES(...)
Definition: Validate.h:545
#define ARM_COMPUTE_RETURN_ERROR_ON_DATA_TYPE_CHANNEL_NOT_IN(t, c,...)
Definition: Validate.h:792
Status validate_arguments(const ITensorInfo *input, const ITensorInfo *bias, const ITensorInfo *output, const GEMMLowpOutputStageInfo *output_stage)
#define ARM_COMPUTE_RETURN_ERROR_ON_MSG(cond, msg)
If the condition is true, an error is returned.
Definition: Error.h:244
static Status validate(ArithmeticOperation op, const ITensorInfo *input1, const ITensorInfo *input2, const ITensorInfo *output)
Static function to check if given info will lead to a valid configuration of cpu::kernels::CpuArithme...
Tensor packing service.
Definition: ITensorPack.h:37
#define ARM_COMPUTE_ERROR_ON_NULLPTR(...)
Definition: Validate.h:161
quantized, asymmetric fixed-point 8-bit number signed
UKernelType * ukernel
static Status validate(const ITensorInfo *input1, const ITensorInfo *input2, const ITensorInfo *output)
Static function to check if given info will lead to a valid configuration of CpuDivisionKernel.
DataType
Available data types.
Definition: Types.h:77
void configure(ArithmeticOperation op, const ITensorInfo *input1, const ITensorInfo *input2, ITensorInfo *output)
Configure kernel.
const char * name
Describe a multidimensional execution window.
Definition: Window.h:39
#define ARM_COMPUTE_ERROR_ON_INVALID_SUBWINDOW(f, s)
Definition: Validate.h:205