Commit f67d92e9 by Daniel Berlin

[multiple changes]

2004-09-16  Daniel Berlin  <dberlin@dberlin.org>

	* cfgloop.h (duplicate_loop):  Add prototype.
	* cfgloopmanip.c (duplicate_loop): Make non-static.
	* lambda-code.c (perfect_nestify): Factor out test whether
	we can handle this loop into separate function.
	Call it.
	(can_convert_to_perfect_nest): New function.
	(replace_uses_of_x_with_y): Add modify_stmt call.
	* tree-loop-linear.c (linear_transform_loops): Call
	rewrite_into_loop_closed_ssa and free_df.

2004-09-16  Daniel Berlin  <dberlin@dberlin.org>

	* lambda-code.c (invariant_in_loop): is_gimple_min_invariant is
	loop invariant as well.
	(perfect_nestify): new function.
	(gcc_loop_to_lambda_loop): New parameters to track lower bounds,
	upper bounds, and steps.
	Set outerinductionvar properly.
	(gcc_loopnest_to_lambda_loopnest): Add loops and need_perfect
	parameters.
	Return NULL if we need a perfect loop and can't make one.
	(lambda_loopnest_to_gcc_loopnest): Correct algorithm.
	(not_interesting_stmt): New function.
	(phi_loop_edge_uses_def): Ditto.
	(stmt_uses_phi_result): Ditto.
	(stmt_is_bumper_for_loop): Ditto.
	(perfect_nest_p): Ditto.
	(nestify_update_pending_stmts): Ditto.
	(replace_uses_of_x_with_y): Ditto.
	(stmt_uses_op): Ditto.
	(perfect_nestify): Ditto.
	* lambda-mat.c (lambda_matrix_id_p): New function.
	* lambda-trans.c (lambda_trans_matrix_id_p): Ditto.
	* lambda.h: Update prototypes.
	* tree-loop-linear (linear_transform_loop): Use new
	perfect_nest_p. Detect and ignore identity transform.
	* tree-ssa-loop.c (pass_linear_transform): Use TODO_write_loop_closed.

2004-09-16  Sebastian Pop  <pop@cri.ensmp.fr>

	* tree-loop-linear.c (gather_interchange_stats): Add more comments.
	Gather also strides of accessed data.  Pass in the data references
	array.
	(try_interchange_loops): Add a new heuristic for handling the temporal
	locality.  Pass in the data references array.
	(linear_transform_loops): Pass the data references array to
	try_interchange_loops.

From-SVN: r87607
parent 83c99486
2004-09-16 Daniel Berlin <dberlin@dberlin.org>
* cfgloop.h (duplicate_loop): Add prototype.
* cfgloopmanip.c (duplicate_loop): Make non-static.
* lambda-code.c (perfect_nestify): Factor out test whether
we can handle this loop into separate function.
Call it.
(can_convert_to_perfect_nest): New function.
(replace_uses_of_x_with_y): Add modify_stmt call.
* tree-loop-linear.c (linear_transform_loops): Call
rewrite_into_loop_closed_ssa and free_df.
2004-09-16 Daniel Berlin <dberlin@dberlin.org>
* lambda-code.c (invariant_in_loop): is_gimple_min_invariant is
loop invariant as well.
(perfect_nestify): new function.
(gcc_loop_to_lambda_loop): New parameters to track lower bounds,
upper bounds, and steps.
Set outerinductionvar properly.
(gcc_loopnest_to_lambda_loopnest): Add loops and need_perfect
parameters.
Return NULL if we need a perfect loop and can't make one.
(lambda_loopnest_to_gcc_loopnest): Correct algorithm.
(not_interesting_stmt): New function.
(phi_loop_edge_uses_def): Ditto.
(stmt_uses_phi_result): Ditto.
(stmt_is_bumper_for_loop): Ditto.
(perfect_nest_p): Ditto.
(nestify_update_pending_stmts): Ditto.
(replace_uses_of_x_with_y): Ditto.
(stmt_uses_op): Ditto.
(perfect_nestify): Ditto.
* lambda-mat.c (lambda_matrix_id_p): New function.
* lambda-trans.c (lambda_trans_matrix_id_p): Ditto.
* lambda.h: Update prototypes.
* tree-loop-linear (linear_transform_loop): Use new
perfect_nest_p. Detect and ignore identity transform.
* tree-ssa-loop.c (pass_linear_transform): Use TODO_write_loop_closed.
2004-09-16 Sebastian Pop <pop@cri.ensmp.fr>
* tree-loop-linear.c (gather_interchange_stats): Add more comments.
Gather also strides of accessed data. Pass in the data references
array.
(try_interchange_loops): Add a new heuristic for handling the temporal
locality. Pass in the data references array.
(linear_transform_loops): Pass the data references array to
try_interchange_loops.
2004-09-16 Kazu Hirata <kazu@cs.umass.edu> 2004-09-16 Kazu Hirata <kazu@cs.umass.edu>
* doc/invoke.texi: Fix typos. Follow spelling conventions. * doc/invoke.texi: Fix typos. Follow spelling conventions.
......
...@@ -308,6 +308,8 @@ extern bool can_duplicate_loop_p (struct loop *loop); ...@@ -308,6 +308,8 @@ extern bool can_duplicate_loop_p (struct loop *loop);
#define DLTHE_FLAG_UPDATE_FREQ 1 /* Update frequencies in #define DLTHE_FLAG_UPDATE_FREQ 1 /* Update frequencies in
duplicate_loop_to_header_edge. */ duplicate_loop_to_header_edge. */
extern struct loop * duplicate_loop (struct loops *, struct loop *,
struct loop *);
extern int duplicate_loop_to_header_edge (struct loop *, edge, struct loops *, extern int duplicate_loop_to_header_edge (struct loop *, edge, struct loops *,
unsigned, sbitmap, edge, edge *, unsigned, sbitmap, edge, edge *,
unsigned *, int); unsigned *, int);
......
...@@ -29,8 +29,6 @@ Software Foundation, 59 Temple Place - Suite 330, Boston, MA ...@@ -29,8 +29,6 @@ Software Foundation, 59 Temple Place - Suite 330, Boston, MA
#include "cfglayout.h" #include "cfglayout.h"
#include "output.h" #include "output.h"
static struct loop * duplicate_loop (struct loops *, struct loop *,
struct loop *);
static void duplicate_subloops (struct loops *, struct loop *, struct loop *); static void duplicate_subloops (struct loops *, struct loop *, struct loop *);
static void copy_loops_to (struct loops *, struct loop **, int, static void copy_loops_to (struct loops *, struct loop **, int,
struct loop *); struct loop *);
...@@ -701,7 +699,7 @@ place_new_loop (struct loops *loops, struct loop *loop) ...@@ -701,7 +699,7 @@ place_new_loop (struct loops *loops, struct loop *loop)
/* Copies copy of LOOP as subloop of TARGET loop, placing newly /* Copies copy of LOOP as subloop of TARGET loop, placing newly
created loop into LOOPS structure. */ created loop into LOOPS structure. */
static struct loop * struct loop *
duplicate_loop (struct loops *loops, struct loop *loop, struct loop *target) duplicate_loop (struct loops *loops, struct loop *loop, struct loop *target)
{ {
struct loop *cloop; struct loop *cloop;
......
...@@ -70,6 +70,29 @@ lambda_matrix_id (lambda_matrix mat, int size) ...@@ -70,6 +70,29 @@ lambda_matrix_id (lambda_matrix mat, int size)
mat[i][j] = (i == j) ? 1 : 0; mat[i][j] = (i == j) ? 1 : 0;
} }
/* Return true if MAT is the identity matrix of SIZE */
bool
lambda_matrix_id_p (lambda_matrix mat, int size)
{
int i, j;
for (i = 0; i < size; i++)
for (j = 0; j < size; j++)
{
if (i == j)
{
if (mat[i][j] != 1)
return false;
}
else
{
if (mat[i][j] != 0)
return false;
}
}
return true;
}
/* Negate the elements of the M x N matrix MAT1 and store it in MAT2. */ /* Negate the elements of the M x N matrix MAT1 and store it in MAT2. */
void void
......
...@@ -45,7 +45,18 @@ lambda_trans_matrix_new (int colsize, int rowsize) ...@@ -45,7 +45,18 @@ lambda_trans_matrix_new (int colsize, int rowsize)
return ret; return ret;
} }
/* Compute the inverse of the transformation. */ /* Return true if MAT is an identity matrix. */
bool
lambda_trans_matrix_id_p (lambda_trans_matrix mat)
{
if (LTM_ROWSIZE (mat) != LTM_COLSIZE (mat))
return false;
return lambda_matrix_id_p (LTM_MATRIX (mat), LTM_ROWSIZE (mat));
}
/* Compute the inverse of the transformation matrix MAT. */
lambda_trans_matrix lambda_trans_matrix
lambda_trans_matrix_inverse (lambda_trans_matrix mat) lambda_trans_matrix_inverse (lambda_trans_matrix mat)
......
...@@ -105,7 +105,9 @@ typedef struct ...@@ -105,7 +105,9 @@ typedef struct
lambda_loopnest lambda_loopnest_new (int, int); lambda_loopnest lambda_loopnest_new (int, int);
lambda_loopnest lambda_loopnest_transform (lambda_loopnest, lambda_trans_matrix); lambda_loopnest lambda_loopnest_transform (lambda_loopnest, lambda_trans_matrix);
struct loop;
struct loops;
bool perfect_nest_p (struct loop *);
bool lambda_transform_legal_p (lambda_trans_matrix, int, varray_type); bool lambda_transform_legal_p (lambda_trans_matrix, int, varray_type);
void print_lambda_loopnest (FILE *, lambda_loopnest, char); void print_lambda_loopnest (FILE *, lambda_loopnest, char);
...@@ -116,6 +118,7 @@ void print_lambda_loop (FILE *, lambda_loop, int, int, char); ...@@ -116,6 +118,7 @@ void print_lambda_loop (FILE *, lambda_loop, int, int, char);
lambda_matrix lambda_matrix_new (int, int); lambda_matrix lambda_matrix_new (int, int);
void lambda_matrix_id (lambda_matrix, int); void lambda_matrix_id (lambda_matrix, int);
bool lambda_matrix_id_p (lambda_matrix, int);
void lambda_matrix_copy (lambda_matrix, lambda_matrix, int, int); void lambda_matrix_copy (lambda_matrix, lambda_matrix, int, int);
void lambda_matrix_negate (lambda_matrix, lambda_matrix, int, int); void lambda_matrix_negate (lambda_matrix, lambda_matrix, int, int);
void lambda_matrix_transpose (lambda_matrix, lambda_matrix, int, int); void lambda_matrix_transpose (lambda_matrix, lambda_matrix, int, int);
...@@ -153,16 +156,17 @@ lambda_trans_matrix lambda_trans_matrix_inverse (lambda_trans_matrix); ...@@ -153,16 +156,17 @@ lambda_trans_matrix lambda_trans_matrix_inverse (lambda_trans_matrix);
void print_lambda_trans_matrix (FILE *, lambda_trans_matrix); void print_lambda_trans_matrix (FILE *, lambda_trans_matrix);
void lambda_matrix_vector_mult (lambda_matrix, int, int, lambda_vector, void lambda_matrix_vector_mult (lambda_matrix, int, int, lambda_vector,
lambda_vector); lambda_vector);
bool lambda_trans_matrix_id_p (lambda_trans_matrix);
lambda_body_vector lambda_body_vector_new (int); lambda_body_vector lambda_body_vector_new (int);
lambda_body_vector lambda_body_vector_compute_new (lambda_trans_matrix, lambda_body_vector lambda_body_vector_compute_new (lambda_trans_matrix,
lambda_body_vector); lambda_body_vector);
void print_lambda_body_vector (FILE *, lambda_body_vector); void print_lambda_body_vector (FILE *, lambda_body_vector);
struct loop; lambda_loopnest gcc_loopnest_to_lambda_loopnest (struct loops *,
struct loop *,
lambda_loopnest gcc_loopnest_to_lambda_loopnest (struct loop *, VEC(tree) **,
VEC(tree) **, VEC(tree) **,
VEC(tree) **); bool);
void lambda_loopnest_to_gcc_loopnest (struct loop *, VEC(tree) *, void lambda_loopnest_to_gcc_loopnest (struct loop *, VEC(tree) *,
VEC(tree) *, VEC(tree) *,
lambda_loopnest, lambda_loopnest,
......
...@@ -55,22 +55,56 @@ Software Foundation, 59 Temple Place - Suite 330, Boston, MA ...@@ -55,22 +55,56 @@ Software Foundation, 59 Temple Place - Suite 330, Boston, MA
transform matrix for locality purposes. transform matrix for locality purposes.
TODO: Completion of partial transforms. */ TODO: Completion of partial transforms. */
/* Gather statistics for loop interchange. Initializes SUM the sum of /* Gather statistics for loop interchange. LOOP_NUMBER is a relative
all the data dependence distances carried by loop LOOP_NUMBER. index in the considered loop nest. The first loop in the
NB_DEPS_NOT_CARRIED_BY_LOOP is initialized to the number of considered loop nest is FIRST_LOOP, and consequently the index of
dependence relations for which the loop LOOP_NUMBER is not carrying the considered loop is obtained by FIRST_LOOP + LOOP_NUMBER.
any dependence. */
Initializes:
- DEPENDENCE_STEPS the sum of all the data dependence distances
carried by loop LOOP_NUMBER,
- NB_DEPS_NOT_CARRIED_BY_LOOP the number of dependence relations
for which the loop LOOP_NUMBER is not carrying any dependence,
- ACCESS_STRIDES the sum of all the strides in LOOP_NUMBER.
Example: for the following loop,
| loop_1 runs 1335 times
| loop_2 runs 1335 times
| A[{{0, +, 1}_1, +, 1335}_2]
| B[{{0, +, 1}_1, +, 1335}_2]
| endloop_2
| A[{0, +, 1336}_1]
| endloop_1
gather_interchange_stats (in loop_1) will return
DEPENDENCE_STEPS = 3002
NB_DEPS_NOT_CARRIED_BY_LOOP = 5
ACCESS_STRIDES = 10694
gather_interchange_stats (in loop_2) will return
DEPENDENCE_STEPS = 3000
NB_DEPS_NOT_CARRIED_BY_LOOP = 7
ACCESS_STRIDES = 8010
*/
static void static void
gather_interchange_stats (varray_type dependence_relations, gather_interchange_stats (varray_type dependence_relations,
varray_type datarefs,
unsigned int loop_number, unsigned int loop_number,
unsigned int *sum, unsigned int first_loop,
unsigned int *nb_deps_not_carried_by_loop) unsigned int *dependence_steps,
unsigned int *nb_deps_not_carried_by_loop,
unsigned int *access_strides)
{ {
unsigned int i; unsigned int i;
*sum = 0; *dependence_steps = 0;
*nb_deps_not_carried_by_loop = 0; *nb_deps_not_carried_by_loop = 0;
*access_strides = 0;
for (i = 0; i < VARRAY_ACTIVE_SIZE (dependence_relations); i++) for (i = 0; i < VARRAY_ACTIVE_SIZE (dependence_relations); i++)
{ {
int dist; int dist;
...@@ -78,11 +112,11 @@ gather_interchange_stats (varray_type dependence_relations, ...@@ -78,11 +112,11 @@ gather_interchange_stats (varray_type dependence_relations,
(struct data_dependence_relation *) (struct data_dependence_relation *)
VARRAY_GENERIC_PTR (dependence_relations, i); VARRAY_GENERIC_PTR (dependence_relations, i);
/* Compute the dependence strides. */
if (DDR_ARE_DEPENDENT (ddr) == chrec_dont_know) if (DDR_ARE_DEPENDENT (ddr) == chrec_dont_know)
{ {
/* Some constants will need tweaking, but not something that should (*dependence_steps) += 0;
be user-accessible. Thus, no --param. */
*sum += 100;
continue; continue;
} }
...@@ -90,34 +124,64 @@ gather_interchange_stats (varray_type dependence_relations, ...@@ -90,34 +124,64 @@ gather_interchange_stats (varray_type dependence_relations,
is no reuse of the data. */ is no reuse of the data. */
if (DDR_ARE_DEPENDENT (ddr) == chrec_known) if (DDR_ARE_DEPENDENT (ddr) == chrec_known)
{ {
/* Ditto on the no --param here */ (*dependence_steps) += 0;
*sum += 1000;
continue; continue;
} }
dist = DDR_DIST_VECT (ddr)[loop_number]; dist = DDR_DIST_VECT (ddr)[loop_number];
if (dist == 0) if (dist == 0)
*nb_deps_not_carried_by_loop++; (*nb_deps_not_carried_by_loop) += 1;
else if (dist < 0) else if (dist < 0)
*sum += -dist; (*dependence_steps) += -dist;
else else
*sum += dist; (*dependence_steps) += dist;
}
/* Compute the access strides. */
for (i = 0; i < VARRAY_ACTIVE_SIZE (datarefs); i++)
{
unsigned int it;
struct data_reference *dr = VARRAY_GENERIC_PTR (datarefs, i);
tree stmt = DR_STMT (dr);
struct loop *stmt_loop = loop_containing_stmt (stmt);
struct loop *inner_loop = current_loops->parray[first_loop + 1];
if (!flow_loop_nested_p (inner_loop, stmt_loop)
&& inner_loop->num != stmt_loop->num)
continue;
for (it = 0; it < DR_NUM_DIMENSIONS (dr); it++)
{
tree chrec = DR_ACCESS_FN (dr, it);
tree tstride = evolution_part_in_loop_num
(chrec, first_loop + loop_number);
if (tstride == NULL_TREE
|| TREE_CODE (tstride) != INTEGER_CST)
continue;
(*access_strides) += int_cst_value (tstride);
}
} }
} }
/* Apply to TRANS any loop interchange that minimize inner loop steps. /* Apply to TRANS any loop interchange that minimize inner loop steps.
DEPTH is the depth of the loop nest, and DEPENDENCE_RELATIONS is an array
of dependence relations.
Returns the new transform matrix. The smaller the reuse vector Returns the new transform matrix. The smaller the reuse vector
distances in the inner loops, the fewer the cache misses. */ distances in the inner loops, the fewer the cache misses.
FIRST_LOOP is the loop->num of the first loop in the analyzed loop
nest. */
static lambda_trans_matrix static lambda_trans_matrix
try_interchange_loops (lambda_trans_matrix trans, try_interchange_loops (lambda_trans_matrix trans,
unsigned int depth, unsigned int depth,
varray_type dependence_relations) varray_type dependence_relations,
varray_type datarefs,
unsigned int first_loop)
{ {
unsigned int loop_i, loop_j; unsigned int loop_i, loop_j;
unsigned int steps_i, steps_j; unsigned int dependence_steps_i, dependence_steps_j;
unsigned int access_strides_i, access_strides_j;
unsigned int nb_deps_not_carried_by_i, nb_deps_not_carried_by_j; unsigned int nb_deps_not_carried_by_i, nb_deps_not_carried_by_j;
struct data_dependence_relation *ddr; struct data_dependence_relation *ddr;
...@@ -132,33 +196,40 @@ try_interchange_loops (lambda_trans_matrix trans, ...@@ -132,33 +196,40 @@ try_interchange_loops (lambda_trans_matrix trans,
for (loop_j = 1; loop_j < depth; loop_j++) for (loop_j = 1; loop_j < depth; loop_j++)
for (loop_i = 0; loop_i < loop_j; loop_i++) for (loop_i = 0; loop_i < loop_j; loop_i++)
{ {
gather_interchange_stats (dependence_relations, loop_i, &steps_i, gather_interchange_stats (dependence_relations, datarefs,
&nb_deps_not_carried_by_i); loop_i, first_loop,
gather_interchange_stats (dependence_relations, loop_j, &steps_j, &dependence_steps_i,
&nb_deps_not_carried_by_j); &nb_deps_not_carried_by_i,
&access_strides_i);
gather_interchange_stats (dependence_relations, datarefs,
loop_j, first_loop,
&dependence_steps_j,
&nb_deps_not_carried_by_j,
&access_strides_j);
/* Heuristics for loop interchange profitability: /* Heuristics for loop interchange profitability:
1. Inner loops should have smallest steps.
2. Inner loops should contain more dependence relations not 1. (spatial locality) Inner loops should have smallest
carried by the loop. dependence steps.
2. (spatial locality) Inner loops should contain more
dependence relations not carried by the loop.
3. (temporal locality) Inner loops should have smallest
array access strides.
*/ */
if (steps_i < steps_j if (dependence_steps_i < dependence_steps_j
|| nb_deps_not_carried_by_i > nb_deps_not_carried_by_j) || nb_deps_not_carried_by_i > nb_deps_not_carried_by_j
|| access_strides_i < access_strides_j)
{ {
lambda_matrix_row_exchange (LTM_MATRIX (trans), loop_i, loop_j); lambda_matrix_row_exchange (LTM_MATRIX (trans), loop_i, loop_j);
/* Validate the resulting matrix. When the transformation /* Validate the resulting matrix. When the transformation
is not valid, reverse to the previous matrix. is not valid, reverse to the previous transformation. */
FIXME: In this case of transformation it could be
faster to verify the validity of the interchange
without applying the transform to the matrix. But for
the moment do it cleanly: this is just a prototype. */
if (!lambda_transform_legal_p (trans, depth, dependence_relations)) if (!lambda_transform_legal_p (trans, depth, dependence_relations))
lambda_matrix_row_exchange (LTM_MATRIX (trans), loop_i, loop_j); lambda_matrix_row_exchange (LTM_MATRIX (trans), loop_i, loop_j);
} }
} }
return trans; return trans;
} }
...@@ -181,6 +252,7 @@ linear_transform_loops (struct loops *loops) ...@@ -181,6 +252,7 @@ linear_transform_loops (struct loops *loops)
lambda_loopnest before, after; lambda_loopnest before, after;
lambda_trans_matrix trans; lambda_trans_matrix trans;
bool problem = false; bool problem = false;
bool need_perfect_nest = false;
/* If it's not a loop nest, we don't want it. /* If it's not a loop nest, we don't want it.
We also don't handle sibling loops properly, We also don't handle sibling loops properly,
which are loops of the following form: which are loops of the following form:
...@@ -197,7 +269,8 @@ linear_transform_loops (struct loops *loops) ...@@ -197,7 +269,8 @@ linear_transform_loops (struct loops *loops)
} */ } */
if (!loop_nest->inner) if (!loop_nest->inner)
continue; continue;
for (temp = loop_nest; temp; temp = temp->inner) depth = 1;
for (temp = loop_nest->inner; temp; temp = temp->inner)
{ {
flow_loop_scan (temp, LOOP_ALL); flow_loop_scan (temp, LOOP_ALL);
/* If we have a sibling loop or multiple exit edges, jump ship. */ /* If we have a sibling loop or multiple exit edges, jump ship. */
...@@ -246,7 +319,15 @@ linear_transform_loops (struct loops *loops) ...@@ -246,7 +319,15 @@ linear_transform_loops (struct loops *loops)
/* Build the transformation matrix. */ /* Build the transformation matrix. */
trans = lambda_trans_matrix_new (depth, depth); trans = lambda_trans_matrix_new (depth, depth);
lambda_matrix_id (LTM_MATRIX (trans), depth); lambda_matrix_id (LTM_MATRIX (trans), depth);
trans = try_interchange_loops (trans, depth, dependence_relations); trans = try_interchange_loops (trans, depth, dependence_relations,
datarefs, loop_nest->num);
if (lambda_trans_matrix_id_p (trans))
{
if (dump_file)
fprintf (dump_file, "Won't transform loop. Optimal transform is the identity transform\n");
continue;
}
/* Check whether the transformation is legal. */ /* Check whether the transformation is legal. */
if (!lambda_transform_legal_p (trans, depth, dependence_relations)) if (!lambda_transform_legal_p (trans, depth, dependence_relations))
...@@ -255,8 +336,12 @@ linear_transform_loops (struct loops *loops) ...@@ -255,8 +336,12 @@ linear_transform_loops (struct loops *loops)
fprintf (dump_file, "Can't transform loop, transform is illegal:\n"); fprintf (dump_file, "Can't transform loop, transform is illegal:\n");
continue; continue;
} }
before = gcc_loopnest_to_lambda_loopnest (loop_nest, &oldivs, if (!perfect_nest_p (loop_nest))
&invariants); need_perfect_nest = true;
before = gcc_loopnest_to_lambda_loopnest (loops,
loop_nest, &oldivs,
&invariants,
need_perfect_nest);
if (!before) if (!before)
continue; continue;
...@@ -279,4 +364,6 @@ linear_transform_loops (struct loops *loops) ...@@ -279,4 +364,6 @@ linear_transform_loops (struct loops *loops)
free_dependence_relations (dependence_relations); free_dependence_relations (dependence_relations);
free_data_refs (datarefs); free_data_refs (datarefs);
} }
rewrite_into_loop_closed_ssa ();
free_df ();
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment