Commit bec6f38730fb96bc6401db8097744c3a13c2af8d
1 parent
a92c8ac7
I do not use trees anymore within the optimized memory management. I realized th…
…at I can calculate the index of an array by the size and use stacks of memory segments under each array element. This should be much faster.
Showing
2 changed files
with
122 additions
and
289 deletions
| @@ -27,6 +27,48 @@ | @@ -27,6 +27,48 @@ | ||
| 27 | 27 | ||
| 28 | #include <sys/types.h> | 28 | #include <sys/types.h> |
| 29 | 29 | ||
| 30 | +/** | ||
| 31 | + * I found this at stanford.edu: | ||
| 32 | + * https://graphics.stanford.edu/~seander/bithacks.html#IntegerLogLookup | ||
| 33 | + * | ||
| 34 | + * Really cool way of dealing with this. The oneat stanford.edu is slightly | ||
| 35 | + * different as ist deals only with 32bit values. I need a 64bit version | ||
| 36 | + * because on a 64bit system size_t is also 64bit and thus it is possible | ||
| 37 | + * to allocate that much amount of memory theoretically. | ||
| 38 | + */ | ||
| 39 | +static | ||
| 40 | +inline | ||
| 41 | +int | ||
| 42 | +bitwidth(size_t value) | ||
| 43 | +{ | ||
| 44 | + static const char LogTable256[256] = { | ||
| 45 | + -1, 0, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, | ||
| 46 | +#define LT(n) n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n | ||
| 47 | + LT(4), LT(5), LT(5), LT(6), LT(6), LT(6), LT(6), | ||
| 48 | + LT(7), LT(7), LT(7), LT(7), LT(7), LT(7), LT(7), LT(7) | ||
| 49 | + }; | ||
| 50 | +#undef LT | ||
| 51 | + | ||
| 52 | + int r; // r will be lg(v) | ||
| 53 | + register size_t t1, t2, t3; // temporaries | ||
| 54 | + | ||
| 55 | + if ((t3 = value >> 32)) { | ||
| 56 | + if ((t2 = t3 >> 16)) { | ||
| 57 | + r = (t1 = t2 >> 8) ? 56 + LogTable256[t1] : 48 + LogTable256[t2]; | ||
| 58 | + } else { | ||
| 59 | + r = (t1 = t3 >> 8) ? 40 + LogTable256[t1] : 32 + LogTable256[t3]; | ||
| 60 | + } | ||
| 61 | + } else { | ||
| 62 | + if ((t2 = value >> 16)) { | ||
| 63 | + r = (t1 = t2 >> 8) ? 24 + LogTable256[t1] : 16 + LogTable256[t2]; | ||
| 64 | + } else { | ||
| 65 | + r = (t1 = value >> 8) ? 8 + LogTable256[t1] : LogTable256[value]; | ||
| 66 | + } | ||
| 67 | + } | ||
| 68 | + | ||
| 69 | + return r; | ||
| 70 | +} | ||
| 71 | + | ||
| 30 | void * TR_malloc(size_t); | 72 | void * TR_malloc(size_t); |
| 31 | void * TR_calloc(size_t, size_t); | 73 | void * TR_calloc(size_t, size_t); |
| 32 | void * TR_reference(void *); | 74 | void * TR_reference(void *); |
| @@ -53,310 +53,73 @@ | @@ -53,310 +53,73 @@ | ||
| 53 | #include <string.h> | 53 | #include <string.h> |
| 54 | #include <search.h> | 54 | #include <search.h> |
| 55 | #include <unistd.h> | 55 | #include <unistd.h> |
| 56 | +#include <stdint.h> | ||
| 56 | 57 | ||
| 57 | #include "tr/memory.h" | 58 | #include "tr/memory.h" |
| 58 | -#include "tr/tree_macros.h" | ||
| 59 | 59 | ||
| 60 | 60 | ||
| 61 | struct memSegment | 61 | struct memSegment |
| 62 | { | 62 | { |
| 63 | size_t ref_count; | 63 | size_t ref_count; |
| 64 | size_t size; | 64 | size_t size; |
| 65 | + int idx; | ||
| 65 | void * ptr; | 66 | void * ptr; |
| 66 | 67 | ||
| 67 | - TR_rbColor color; | ||
| 68 | - | ||
| 69 | - struct memSegment * data; | ||
| 70 | - | ||
| 71 | struct memSegment * next; | 68 | struct memSegment * next; |
| 72 | - struct memSegment * last; | ||
| 73 | - | ||
| 74 | - struct memSegment * parent; | ||
| 75 | - struct memSegment * left; | ||
| 76 | - struct memSegment * right; | ||
| 77 | }; | 69 | }; |
| 78 | 70 | ||
| 79 | static | 71 | static |
| 80 | struct memSegment * | 72 | struct memSegment * |
| 81 | -newElement(size_t size) | 73 | +newElement(size_t size, int idx) |
| 82 | { | 74 | { |
| 83 | struct memSegment * element = malloc(size); | 75 | struct memSegment * element = malloc(size); |
| 84 | 76 | ||
| 85 | element->ref_count = 1; | 77 | element->ref_count = 1; |
| 86 | element->size = size; | 78 | element->size = size; |
| 79 | + element->idx = idx; | ||
| 87 | element->ptr = (void*)element + sizeof(struct memSegment); | 80 | element->ptr = (void*)element + sizeof(struct memSegment); |
| 88 | 81 | ||
| 89 | - element->data = element; | ||
| 90 | - | ||
| 91 | element->next = NULL; | 82 | element->next = NULL; |
| 92 | - element->last = NULL; | ||
| 93 | - | ||
| 94 | - element->color = rbRed; | ||
| 95 | - element->parent = NULL; | ||
| 96 | - element->left = NULL; | ||
| 97 | - element->right = NULL; | ||
| 98 | 83 | ||
| 99 | return element; | 84 | return element; |
| 100 | } | 85 | } |
| 101 | 86 | ||
| 102 | #ifdef MEM_OPT | 87 | #ifdef MEM_OPT |
| 103 | -static | ||
| 104 | -int | ||
| 105 | -_memSegmentFindCompare(const void * a, const void * b) | ||
| 106 | -{ | ||
| 107 | - struct memSegment * _a = (struct memSegment *)a; | ||
| 108 | - size_t _b = *(size_t *)b; | ||
| 109 | - | ||
| 110 | - /* | ||
| 111 | - * find the smallest bigger or equal size segment | ||
| 112 | - */ | ||
| 113 | - return _a->size < _b ? -1 | ||
| 114 | - : _a->size > _b && _a->left && _a->left->size >= _b ? 1 : 0; | ||
| 115 | -} | ||
| 116 | - | ||
| 117 | -static | ||
| 118 | -int | ||
| 119 | -_memSegmentCompare(const void * a, const void * b) | ||
| 120 | -{ | ||
| 121 | - size_t _a = ((struct memSegment *)a)->size; | ||
| 122 | - size_t _b = ((struct memSegment *)b)->size; | ||
| 123 | - | ||
| 124 | - return _a < _b ? -1 : _a > _b ? 1 : 0; | ||
| 125 | -} | ||
| 126 | - | ||
| 127 | /** | 88 | /** |
| 128 | * insert element in tree | 89 | * insert element in tree |
| 129 | */ | 90 | */ |
| 130 | static | 91 | static |
| 92 | +inline | ||
| 131 | struct memSegment * | 93 | struct memSegment * |
| 132 | -insertElement(struct memSegment ** tree, struct memSegment * element) | 94 | +insertElement(struct memSegment ** stack, struct memSegment * element) |
| 133 | { | 95 | { |
| 134 | - struct memSegment * node = *tree; | ||
| 135 | - struct memSegment * new_node = NULL; | ||
| 136 | - int found; | ||
| 137 | - | ||
| 138 | - element->next = NULL; | ||
| 139 | - element->last = NULL; | ||
| 140 | - | ||
| 141 | - element->color = rbRed; | ||
| 142 | - element->parent = NULL; | ||
| 143 | - element->left = NULL; | ||
| 144 | - element->right = NULL; | ||
| 145 | - | ||
| 146 | - TR_TREE_FIND(node, element, found, _memSegmentCompare); | ||
| 147 | - | ||
| 148 | - // if tree is empty it's simple... :) | ||
| 149 | - if (NULL == node) { | ||
| 150 | - *tree = node = new_node = element; | ||
| 151 | - } else { | ||
| 152 | - // normal binary tree add.... | ||
| 153 | - if (found == 0) { | ||
| 154 | - if (NULL == node->next) { | ||
| 155 | - node->next = element; | ||
| 156 | - node->last = element; | ||
| 157 | - } else { | ||
| 158 | - node->last->next = element; | ||
| 159 | - node->last = element; | ||
| 160 | - } | ||
| 161 | - return node; | ||
| 162 | - } else { | ||
| 163 | - if (0 < found) { | ||
| 164 | - node->left = element; | ||
| 165 | - node->left->parent = node; | ||
| 166 | - new_node = node = node->left; | ||
| 167 | - } else { | ||
| 168 | - node->right = element; | ||
| 169 | - node->right->parent = node; | ||
| 170 | - new_node = node = node->right; | 96 | + element->next = *stack; |
| 97 | + *stack = element; | ||
| 171 | 98 | ||
| 172 | - } | ||
| 173 | - } | ||
| 174 | - } | ||
| 175 | - | ||
| 176 | - /* | ||
| 177 | - * handle reballancing rb style | ||
| 178 | - */ | ||
| 179 | - TR_TREE_BALANCE_INSERT(tree, node); | ||
| 180 | - | ||
| 181 | - return new_node; | 99 | + return element; |
| 182 | } | 100 | } |
| 183 | 101 | ||
| 184 | static | 102 | static |
| 103 | +inline | ||
| 185 | struct memSegment * | 104 | struct memSegment * |
| 186 | -deleteElement(struct memSegment ** tree, size_t size) | 105 | +deleteElement(struct memSegment ** stack) |
| 187 | { | 106 | { |
| 188 | - struct memSegment * node = *tree; | ||
| 189 | - struct memSegment * del_node; | ||
| 190 | - struct memSegment * child; | ||
| 191 | - struct memSegment * s; | ||
| 192 | - int found; | ||
| 193 | - | ||
| 194 | - // find the relevant node and it's parent | ||
| 195 | - TR_TREE_FIND(node, &size, found, _memSegmentFindCompare); | 107 | + struct memSegment * del_node = *stack; |
| 196 | 108 | ||
| 197 | - //while (node) { | ||
| 198 | - if (found != 0) { | ||
| 199 | - return NULL; | ||
| 200 | - } else { | ||
| 201 | - if (NULL != node->next) { | ||
| 202 | - if (NULL != node->parent) { | ||
| 203 | - if (node == node->parent->left) { | ||
| 204 | - node->parent->left = node->next; | ||
| 205 | - } else { | ||
| 206 | - node->parent->right = node->next; | ||
| 207 | - } | ||
| 208 | - } else { | ||
| 209 | - *tree = node->next; | ||
| 210 | - } | ||
| 211 | - | ||
| 212 | - if (NULL != node->left) { | ||
| 213 | - node->left->parent = node->next; | ||
| 214 | - } | ||
| 215 | - | ||
| 216 | - if (NULL != node->right) { | ||
| 217 | - node->right->parent = node->next; | ||
| 218 | - } | ||
| 219 | - | ||
| 220 | - node->next->last = node->last; | ||
| 221 | - node->next->color = node->color; | ||
| 222 | - node->next->parent = node->parent; | ||
| 223 | - node->next->left = node->left; | ||
| 224 | - node->next->right = node->right; | ||
| 225 | - | ||
| 226 | - return node; | ||
| 227 | - } | 109 | + if (*stack) { |
| 110 | + *stack = (*stack)->next; | ||
| 228 | } | 111 | } |
| 229 | 112 | ||
| 230 | - del_node = node; | ||
| 231 | - | ||
| 232 | - // now our cases follows...the first one is the same as with | ||
| 233 | - // simple binary search trees. Two non null children. | ||
| 234 | - | ||
| 235 | - // case 1: two children | ||
| 236 | - if (NULL != node->left && NULL != node->right) { | ||
| 237 | - struct memSegment * successor; | ||
| 238 | - struct memSegment * tmpparent; | ||
| 239 | - struct memSegment * tmpleft; | ||
| 240 | - struct memSegment * tmpright; | ||
| 241 | - TR_rbColor tmpcolor; | ||
| 242 | - | ||
| 243 | - TR_TREE_INORDER_SUCC(node, successor); | ||
| 244 | - tmpparent = successor->parent; | ||
| 245 | - tmpleft = successor->left; | ||
| 246 | - tmpright = successor->right; | ||
| 247 | - tmpcolor = successor->color; | ||
| 248 | - | ||
| 249 | - TR_TREE_REPLACE_NODE(tree, node, successor); | ||
| 250 | - | ||
| 251 | - successor->color = node->color; | ||
| 252 | - successor->left = node->left; | ||
| 253 | - successor->left->parent = successor; | ||
| 254 | - // the right one might be successor... | ||
| 255 | - if (node->right == successor) { | ||
| 256 | - successor->right = node; | ||
| 257 | - node->parent = successor; | ||
| 258 | - } else { | ||
| 259 | - successor->right = node->right; | ||
| 260 | - node->right->parent = successor; | ||
| 261 | - node->parent = tmpparent; | ||
| 262 | - tmpparent->left = node; | ||
| 263 | - } | ||
| 264 | - | ||
| 265 | - node->color = tmpcolor; | ||
| 266 | - node->left = tmpleft; | ||
| 267 | - node->right = tmpright; | ||
| 268 | - } | ||
| 269 | - | ||
| 270 | - // Precondition: n has at most one non-null child. | ||
| 271 | - child = (NULL == node->right) ? node->left : node->right; | ||
| 272 | - TR_TREE_REPLACE_NODE(tree, node, child); | ||
| 273 | - | ||
| 274 | - // delete one child case | ||
| 275 | - // TODO this is overly complex as simply derived from the function... | ||
| 276 | - // maybe this can be simplified. Maybe not...check. | ||
| 277 | - if (node->color == rbBlack) { | ||
| 278 | - if (NULL != child && child->color == rbRed) { | ||
| 279 | - child->color = rbBlack; | ||
| 280 | - // done despite modifying tree itself if neccessary.. | ||
| 281 | - return del_node; | ||
| 282 | - } else { | ||
| 283 | - if (NULL != child) { | ||
| 284 | - node = child; | ||
| 285 | - } else { | ||
| 286 | - node->color = rbBlack; | ||
| 287 | - node->left = NULL; | ||
| 288 | - node->right = NULL; | ||
| 289 | - } | ||
| 290 | - } | ||
| 291 | - } else { | ||
| 292 | - return del_node; | ||
| 293 | - } | ||
| 294 | - | ||
| 295 | - TR_TREE_BALANCE_DELETE(tree, node, s); | ||
| 296 | - | ||
| 297 | - return del_node; | 113 | + return del_node; |
| 298 | } | 114 | } |
| 299 | 115 | ||
| 300 | -static | ||
| 301 | -void | ||
| 302 | -post(struct memSegment * tree, void (*cb)(struct memSegment *, int)) | ||
| 303 | -{ | ||
| 304 | - struct memSegment * previous = tree; | ||
| 305 | - struct memSegment * node = tree; | ||
| 306 | - int depth = 1; | ||
| 307 | - | ||
| 308 | - /* | ||
| 309 | - * I think this has something like O(n+log(n)) on a ballanced | ||
| 310 | - * tree because I have to traverse back the rightmost leaf to | ||
| 311 | - * the root to get a break condition. | ||
| 312 | - */ | ||
| 313 | - while (node) { | ||
| 314 | - /* | ||
| 315 | - * If we come from the right so nothing and go to our | ||
| 316 | - * next parent. | ||
| 317 | - */ | ||
| 318 | - if (((NULL == node->left || previous == node->left) | ||
| 319 | - && NULL == node->right) | ||
| 320 | - || previous == node->right) { | ||
| 321 | - | ||
| 322 | - struct memSegment * parent = node->parent; | ||
| 323 | - | ||
| 324 | - cb(node, depth); | ||
| 325 | - | ||
| 326 | - previous = node; | ||
| 327 | - node = parent; | ||
| 328 | - depth--; | ||
| 329 | - continue; | ||
| 330 | - } | 116 | +#define TR_MAX_MEM_IDX 1024 |
| 331 | 117 | ||
| 332 | - if ((NULL == node->left || previous == node->left)) { | ||
| 333 | - /* | ||
| 334 | - * If there are no more elements to the left or we | ||
| 335 | - * came from the left, process data. | ||
| 336 | - */ | ||
| 337 | - previous = node; | ||
| 338 | - | ||
| 339 | - if (NULL != node->right) { | ||
| 340 | - node = node->right; | ||
| 341 | - depth++; | ||
| 342 | - } else { | ||
| 343 | - node = node->parent; | ||
| 344 | - depth--; | ||
| 345 | - } | ||
| 346 | - } else { | ||
| 347 | - /* | ||
| 348 | - * if there are more elements to the left go there. | ||
| 349 | - */ | ||
| 350 | - previous = node; | ||
| 351 | - node = node->left; | ||
| 352 | - depth++; | ||
| 353 | - } | ||
| 354 | - } | ||
| 355 | -} | ||
| 356 | - | ||
| 357 | -struct memSegment * segments = NULL; | 118 | +struct memSegment * segments[TR_MAX_MEM_IDX] = {}; |
| 119 | +pthread_mutex_t TR_memop_lock = PTHREAD_MUTEX_INITIALIZER; | ||
| 358 | 120 | ||
| 359 | static | 121 | static |
| 122 | +inline | ||
| 360 | void | 123 | void |
| 361 | segmentFree(struct memSegment * segment, int depth) | 124 | segmentFree(struct memSegment * segment, int depth) |
| 362 | { | 125 | { |
| @@ -378,8 +141,6 @@ TR_reference(void * mem) | @@ -378,8 +141,6 @@ TR_reference(void * mem) | ||
| 378 | return mem; | 141 | return mem; |
| 379 | } | 142 | } |
| 380 | 143 | ||
| 381 | -pthread_mutex_t TR_memop_lock = PTHREAD_MUTEX_INITIALIZER; | ||
| 382 | - | ||
| 383 | /* | 144 | /* |
| 384 | * This tries to reflect the memory management behaviour of the | 145 | * This tries to reflect the memory management behaviour of the |
| 385 | * GNU version of malloc. For other versions this might need | 146 | * GNU version of malloc. For other versions this might need |
| @@ -410,41 +171,60 @@ pthread_mutex_t TR_memop_lock = PTHREAD_MUTEX_INITIALIZER; | @@ -410,41 +171,60 @@ pthread_mutex_t TR_memop_lock = PTHREAD_MUTEX_INITIALIZER; | ||
| 410 | void * | 171 | void * |
| 411 | TR_malloc(size_t size) | 172 | TR_malloc(size_t size) |
| 412 | { | 173 | { |
| 413 | - struct memSegment * seg = NULL; | ||
| 414 | - long psize = sysconf(_SC_PAGESIZE); | 174 | + struct memSegment * seg = NULL; |
| 175 | + long psize = sysconf(_SC_PAGESIZE); | ||
| 176 | + static int psize_width = 0; | ||
| 177 | + int idx; | ||
| 178 | + | ||
| 179 | + if (psize_width == 0) psize_width = bitwidth(psize); | ||
| 415 | 180 | ||
| 416 | size += sizeof(struct memSegment); | 181 | size += sizeof(struct memSegment); |
| 417 | 182 | ||
| 418 | - if (size > psize) { | ||
| 419 | - if (0 != (size % psize)) { | ||
| 420 | - // size if not a multiple of pagesize so bring it to one. | ||
| 421 | - size = ((size / psize) + 1) * psize; | ||
| 422 | - } | ||
| 423 | - } else { | ||
| 424 | - if (size < 8) { | ||
| 425 | - size = 8; | ||
| 426 | - } else { | ||
| 427 | - size_t check = size >> 4; | ||
| 428 | - size_t mask = 0x1F; | 183 | +#define MIN_BITS 8 |
| 429 | 184 | ||
| 430 | - while (check >>= 1) { | ||
| 431 | - mask = (mask << 1) | 1; | ||
| 432 | - } | 185 | + if (size >= psize) { |
| 186 | + idx = size / psize; | ||
| 433 | 187 | ||
| 434 | - if (size != (size & ~(mask >> 1))) { | ||
| 435 | - size = (size << 1) & ~mask; | ||
| 436 | - } | ||
| 437 | - } | ||
| 438 | - } | 188 | + if (0 != (size % psize)) { |
| 189 | + // size if not a multiple of pagesize so bring it to one. | ||
| 190 | + size = (idx + 1) * psize; | ||
| 191 | + idx++; | ||
| 192 | + } | ||
| 193 | + | ||
| 194 | + idx += psize_width - MIN_BITS; | ||
| 195 | + } else { | ||
| 196 | + if (size <= 1 << (MIN_BITS - 1)) { | ||
| 197 | + size = 1 << (MIN_BITS - 1); | ||
| 198 | + idx = 0; | ||
| 199 | + } else { | ||
| 200 | + size_t mask; | ||
| 201 | + | ||
| 202 | + idx = bitwidth(size); | ||
| 203 | + mask = (1 << (idx + 1)) - 1; | ||
| 204 | + idx -= (MIN_BITS - 1); | ||
| 205 | + | ||
| 206 | + if (size != (size & ~(mask >> 1))) { | ||
| 207 | + size = (size << 1) & ~mask; | ||
| 208 | + idx++; | ||
| 209 | + } | ||
| 210 | + } | ||
| 211 | + } | ||
| 212 | + | ||
| 213 | +#undef MIN_BITS | ||
| 439 | 214 | ||
| 440 | #ifdef MEM_OPT | 215 | #ifdef MEM_OPT |
| 441 | - pthread_mutex_lock(&TR_memop_lock); | ||
| 442 | - seg = deleteElement(&segments, size); | ||
| 443 | - pthread_mutex_unlock(&TR_memop_lock); | 216 | + if (idx < TR_MAX_MEM_IDX) { |
| 217 | + pthread_mutex_lock(&TR_memop_lock); | ||
| 218 | + seg = deleteElement(&(segments[idx])); | ||
| 219 | + pthread_mutex_unlock(&TR_memop_lock); | ||
| 220 | + } else | ||
| 444 | #endif | 221 | #endif |
| 222 | + { | ||
| 223 | + idx = -1; | ||
| 224 | + } | ||
| 445 | 225 | ||
| 446 | if (NULL == seg) { | 226 | if (NULL == seg) { |
| 447 | - seg = newElement(size); | 227 | + seg = newElement(size, idx); |
| 448 | } | 228 | } |
| 449 | 229 | ||
| 450 | return seg->ptr; | 230 | return seg->ptr; |
| @@ -479,12 +259,15 @@ TR_free(void ** mem) | @@ -479,12 +259,15 @@ TR_free(void ** mem) | ||
| 479 | seg->ref_count--; | 259 | seg->ref_count--; |
| 480 | } else { | 260 | } else { |
| 481 | #ifdef MEM_OPT | 261 | #ifdef MEM_OPT |
| 482 | - pthread_mutex_lock(&TR_memop_lock); | ||
| 483 | - insertElement(&segments, seg); | ||
| 484 | - pthread_mutex_unlock(&TR_memop_lock); | ||
| 485 | -#else | ||
| 486 | - free(seg); | 262 | + if (-1 != seg->idx) { |
| 263 | + pthread_mutex_lock(&TR_memop_lock); | ||
| 264 | + insertElement(&(segments[seg->idx]), seg); | ||
| 265 | + pthread_mutex_unlock(&TR_memop_lock); | ||
| 266 | + } else | ||
| 487 | #endif | 267 | #endif |
| 268 | + { | ||
| 269 | + free(seg); | ||
| 270 | + } | ||
| 488 | } | 271 | } |
| 489 | 272 | ||
| 490 | *mem = NULL; | 273 | *mem = NULL; |
| @@ -508,7 +291,15 @@ void | @@ -508,7 +291,15 @@ void | ||
| 508 | TR_cleanup() | 291 | TR_cleanup() |
| 509 | { | 292 | { |
| 510 | #ifdef MEM_OPT | 293 | #ifdef MEM_OPT |
| 511 | - post(segments, segmentFree); | 294 | + int i; |
| 295 | + | ||
| 296 | + for (i=0; i<TR_MAX_MEM_IDX; i++) { | ||
| 297 | + while(segments[i]) { | ||
| 298 | + struct memSegment * next = segments[i]->next; | ||
| 299 | + free(segments[i]); | ||
| 300 | + segments[i] = next; | ||
| 301 | + } | ||
| 302 | + } | ||
| 512 | #endif | 303 | #endif |
| 513 | } | 304 | } |
| 514 | 305 |
Please
register
or
login
to post a comment